Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3669a6e7dc | ||
|
|
cd54fe4a0f | ||
|
|
1e7a6d974f | ||
|
|
61f226e86d | ||
|
|
6809d357c0 | ||
|
|
502a6010f1 | ||
|
|
f9f2e116e0 | ||
|
|
479fecd2c6 | ||
|
|
df8d088fac | ||
|
|
b570e9410d | ||
|
|
e9c2599542 | ||
|
|
a8a6ce3b29 | ||
|
|
0944b1d93e | ||
|
|
1c8f285151 | ||
|
|
041ede7f8d | ||
|
|
33032f8fb0 | ||
|
|
3249c8e942 | ||
|
|
24a0694b80 | ||
|
|
a494d320d7 | ||
|
|
7957cda438 | ||
|
|
dcee0096fe | ||
|
|
b74e98ffbb | ||
|
|
5aa5a484a7 | ||
|
|
464b0d20b8 | ||
|
|
ae69c5ce31 | ||
|
|
8aa22e6591 | ||
|
|
3d1881f4bc | ||
|
|
2935752613 | ||
|
|
a969898a16 | ||
|
|
57492d6759 | ||
|
|
ba9ea7743f | ||
|
|
84dc34c0a3 | ||
|
|
34a9862591 | ||
|
|
f3139d2b5d | ||
|
|
6e573d5551 | ||
|
|
2210bf5c40 | ||
|
|
dc78114102 | ||
|
|
259e1b2ca8 | ||
|
|
148a039545 | ||
|
|
25dbfb2b61 | ||
|
|
fa69edc1aa | ||
|
|
9cf04c427e | ||
|
|
f446da8ae6 | ||
|
|
c2a54bd25d | ||
|
|
56668ee43d | ||
|
|
c8e5a7162b | ||
|
|
00b64b9fdc | ||
|
|
7c23ebf382 |
@@ -1,11 +1,5 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mc2-memory": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
|
||||
]
|
||||
},
|
||||
"mc2-system": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
|
||||
+55
-37
@@ -1,37 +1,55 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "mc2",
|
||||
"runtimeExecutable": "F:\\Coding Stuff\\mission-control-2\\backend\\.venv\\Scripts\\python.exe",
|
||||
"runtimeArgs": [
|
||||
"-m",
|
||||
"uvicorn",
|
||||
"app:app",
|
||||
"--app-dir",
|
||||
"F:\\Coding Stuff\\mission-control-2\\backend",
|
||||
"--port",
|
||||
"9000"
|
||||
],
|
||||
"port": 9000
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev", "--", "--port", "5180", "--strictPort"],
|
||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
|
||||
"autoPort": false,
|
||||
"port": 5180
|
||||
},
|
||||
{
|
||||
"name": "frontend-mock",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
|
||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
|
||||
"autoPort": false,
|
||||
"port": 5181
|
||||
}
|
||||
]
|
||||
}
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "mc2",
|
||||
"runtimeExecutable": "F:\\Coding Stuff\\mission-control-2\\backend\\.venv\\Scripts\\python.exe",
|
||||
"runtimeArgs": [
|
||||
"-m",
|
||||
"uvicorn",
|
||||
"app:app",
|
||||
"--app-dir",
|
||||
"F:\\Coding Stuff\\mission-control-2\\backend",
|
||||
"--port",
|
||||
"9000"
|
||||
],
|
||||
"port": 9000
|
||||
},
|
||||
{
|
||||
"name": "frontend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev",
|
||||
"--",
|
||||
"--port",
|
||||
"5180",
|
||||
"--strictPort"
|
||||
],
|
||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||
"env": {
|
||||
"MC_API_TARGET": "http://192.168.178.151:9001"
|
||||
},
|
||||
"autoPort": false,
|
||||
"port": 5180
|
||||
},
|
||||
{
|
||||
"name": "frontend-mock",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"dev",
|
||||
"--",
|
||||
"--port",
|
||||
"5181",
|
||||
"--strictPort"
|
||||
],
|
||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||
"env": {
|
||||
"MC_API_TARGET": "http://127.0.0.1:9000"
|
||||
},
|
||||
"autoPort": false,
|
||||
"port": 5181
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
#
|
||||
# Darum prueft die MC2-Ampel, was im Container EHRLICH gruen sein kann und trotzdem echte
|
||||
# Fehler faengt: Lint (ruff, Projekt-Politik in ruff.toml) + Import/Syntax (compileall) +
|
||||
# Frontend-Build inkl. TypeScript-Typecheck (tsc). Rot ist ein Ergebnis, kein Aergernis.
|
||||
# Frontend-Build inkl. TypeScript-Typecheck (tsc) + Frontend-Tests (vitest/jsdom) + ESLint
|
||||
# + Buendel-Budget. Rot ist ein Ergebnis, kein Aergernis.
|
||||
name: Ampel
|
||||
on: [push, pull_request]
|
||||
|
||||
@@ -42,7 +43,65 @@ jobs:
|
||||
rot=1
|
||||
else
|
||||
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
||||
|
||||
|
||||
# Frontend-Tests + Linter (v3-Umbau P1, 28.08.2026).
|
||||
# Bis dahin lief das Frontend als einziger Teil des Stacks voellig ungeprueft
|
||||
# durch das Gate — 10 500 Zeilen, null Tests. Die Begruendung weiter oben
|
||||
# ("echte Tests sind der Pruefstand mit LIVE-Diensten") gilt fuer die
|
||||
# ML-schweren Python-Dienste, nicht fuer Frontend-Unit-Tests: die laufen in
|
||||
# jsdom, brauchen weder Modell noch GPU und sind in Sekunden durch.
|
||||
# Der Linter meldet 0 Fehler / ~90 Warnungen; rot wird nur bei Fehlern.
|
||||
if [ $rot -eq 0 ]; then
|
||||
echo "-- Vitest"
|
||||
( cd frontend && npm test --silent ) || rot=1
|
||||
echo "-- ESLint (Warnungen sind erlaubt, Fehler nicht)"
|
||||
( cd frontend && npm run lint --silent ) || rot=1
|
||||
fi
|
||||
|
||||
# Bündel-Budget (v3-Umbau P0, 28.08.2026): Der Start-Chunk ist das, was der
|
||||
# Nutzer VOR dem ersten Bild lädt. Er lag bei 220 kB gzip, weil das eifrig
|
||||
# geladene Cockpit Recharts mitzog; dist lag bei 27 MB wegen eines verwaisten
|
||||
# Avatar-Modells. Ohne Deckel wächst beides unbemerkt zurück. Gemessen wird der
|
||||
# FRISCHE Build im Runner — kein Vergleich mit dem committeten dist (das wäre
|
||||
# über Node-Versionen hinweg flatterhaft und würde dauerhaft rot leuchten).
|
||||
if [ $rot -eq 0 ] && [ -d frontend/dist/assets ]; then
|
||||
# 28.08.2026, React 18 -> 19: Der Sprung kostete GEMESSEN 14 645 B gzip
|
||||
# (118 762 -> 133 407) und riss das bis dahin geltende Budget von 125 000.
|
||||
# Das Gate hat also getan, was es soll. Angehoben wurde es TROTZDEM —
|
||||
# bewusst und einmalig, nicht weil es im Weg stand:
|
||||
# · Der Zuwachs IST die Plattform, nicht Wildwuchs. Es gibt hier nichts
|
||||
# wegzulassen, so wie beim Router (P2), wo Schublade und Palette
|
||||
# hinter lazy() wanderten und das Budget scharf blieb.
|
||||
# · MC2 ist eine LAN-Appliance. 14 kB sind ueber Gigabit-Ethernet keine
|
||||
# messbare Wartezeit; das Budget existiert gegen DRIFT (ein 24-MB-Avatar,
|
||||
# eine 95-kB-Diagramm-Bibliothek auf der Startseite), nicht gegen einen
|
||||
# ueberlegten Plattform-Schritt.
|
||||
# Der Abstand zum Budget bleibt derselbe wie vorher (~5 %).
|
||||
budget_gz=140000 # Stand nach React 19: 133 407 B gzip
|
||||
budget_dist=3145728 # Stand nach P0: 1 477 852 B
|
||||
# Den Einstiegs-Chunk aus index.html lesen, NICHT per Glob raten:
|
||||
# Rollup nennt auch kleine geteilte Module "index-*.js" (gemessen: ein
|
||||
# 67-Byte-Chunk neben dem 375-kB-Einstieg). `ls | head -1` haette je nach
|
||||
# Hash den falschen erwischt — und das Budget waere still immer gruen.
|
||||
einstieg=$(grep -o 'assets/index-[A-Za-z0-9_-]*\.js' frontend/dist/index.html | head -1)
|
||||
haupt="frontend/dist/$einstieg"
|
||||
if [ -n "$haupt" ]; then
|
||||
gz=$(gzip -c "$haupt" | wc -c)
|
||||
echo "-- Start-Chunk: $gz B gzip (Budget $budget_gz)"
|
||||
if [ "$gz" -gt "$budget_gz" ]; then
|
||||
echo "❌ Start-Bündel über Budget. Meist eine neue Bibliothek, die über"
|
||||
echo " das eifrig geladene Cockpit hereinkommt — hinter lazy() legen."
|
||||
rot=1
|
||||
fi
|
||||
fi
|
||||
gesamt=$(du -sb frontend/dist | cut -f1)
|
||||
echo "-- dist gesamt: $gesamt B (Budget $budget_dist)"
|
||||
if [ "$gesamt" -gt "$budget_dist" ]; then
|
||||
echo "❌ dist über Budget — Ballast? (Schrift-Subsets, Medien, WOFF 1)"
|
||||
rot=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $rot -eq 0 ]; then
|
||||
echo "== Frontend: Push nach release-dist =="
|
||||
git config --global user.name "Gitea Actions"
|
||||
|
||||
+19
-24
@@ -1,24 +1,19 @@
|
||||
# Python
|
||||
backend/.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Node / Vite
|
||||
frontend/node_modules/
|
||||
# frontend/dist wird committet (kein Node-Build auf der Box) — siehe deploy/
|
||||
|
||||
# Avatar-VRM (groß + lizenz-/redistributionssensibel) — liegt lokal + auf der Box, nicht in git.
|
||||
# Wird per Direkt-Deploy auf die Box gespielt (dist/avatar.vrm), nicht über git.
|
||||
frontend/public/avatar.vrm
|
||||
frontend/dist/avatar.vrm
|
||||
|
||||
# Env / local
|
||||
*.env
|
||||
.DS_Store
|
||||
|
||||
# Box-Recon-/Scratch-Skripte (lokale Diagnose, nicht fürs Repo)
|
||||
box_recon*
|
||||
gemma_swap*
|
||||
|
||||
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||
frontend/tsconfig.tsbuildinfo
|
||||
# Python
|
||||
backend/.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Node / Vite
|
||||
frontend/node_modules/
|
||||
# frontend/dist wird committet (kein Node-Build auf der Box) — siehe deploy/
|
||||
|
||||
# Env / local
|
||||
*.env
|
||||
.DS_Store
|
||||
|
||||
# Box-Recon-/Scratch-Skripte (lokale Diagnose, nicht fürs Repo)
|
||||
box_recon*
|
||||
gemma_swap*
|
||||
|
||||
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||
frontend/tsconfig.tsbuildinfo
|
||||
|
||||
@@ -1,60 +1,67 @@
|
||||
# AGENTS.md — Mission Control 2.0
|
||||
|
||||
Projekt-Geschmack für Coding-Agenten (Kilo Code, Claude Code lesen diese Datei).
|
||||
Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `README.md`, `docs/`.
|
||||
|
||||
## Was das ist
|
||||
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
||||
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
|
||||
**Hermes-Agent (das autonome Gehirn "Lucy")**. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
||||
|
||||
**Hardware-Spezifikationen der Box (WICHTIG für alle Agenten):**
|
||||
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
||||
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
||||
**Gedächtnis & Dienste:** Hermes ist das autonome Agenten-Gehirn („Lucy"). Mem0 läuft als dedizierter semantischer Langzeit-Gedächtnis-Sidecar (`mem0-service` auf `:8765`) und wird von Hermes via MCP (`mcp_memory.py`) eingebunden. Governor, Zed-Workflows und alte Mittelschichten sind komplett gelöscht.
|
||||
|
||||
## Sprache (nicht verhandelbar)
|
||||
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
||||
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
|
||||
- **Hermes' INTERNE System-Prompts bleiben Englisch** (fremde Software, wir forken sie nicht).
|
||||
Antwort-Sprache ≠ Prompt-Sprache — Deutsch kommt aus SOUL.md, nicht aus den Tool-Prompts.
|
||||
|
||||
## Build & Deploy (die häufigste Falle)
|
||||
- **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die
|
||||
gebauten Assets direkt aus. Nach jeder Frontend-Änderung: `cd frontend && npm run build`, dann
|
||||
**das neue `frontend/dist` mit-committen**. Vergessen = Box zeigt alten Stand.
|
||||
(Ausnahme: `frontend/dist/avatar.vrm` ist bewusst nicht in git — siehe `.gitignore`.)
|
||||
- **Deploy macht `git reset --hard origin/main`** (`deploy/deploy.sh`). Heißt: **`main` muss vor
|
||||
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
|
||||
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
||||
|
||||
## Agentic IDE & Vibe Coding
|
||||
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
|
||||
- Es gibt keinen Zed-Workflow, keine OpenCode-CLI-Mittelschicht und keinen Governor mehr.
|
||||
- Der Agent in OpenCode Desktop nutzt via MCP (`.agents/mcp_config.json`) die API der Box (`:9001/v1`), um autonom Projekte zu bauen.
|
||||
- **Mix-Ansatz beim Testen:** Der Agent testet lokal. Vor dem Push muss er prüfen, ob das Gitea-`VERIFY`-Skript fehlerfrei durchläuft.
|
||||
|
||||
## Zeit & Umgebung
|
||||
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
||||
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
||||
(siehe `backend/services/reminders.py`).
|
||||
|
||||
## Backend-Konventionen
|
||||
- Router unter `backend/routers/` (`APIRouter(prefix="/api")`), Logik in `backend/services/`
|
||||
(Single Source of Truth — Router bleiben dünn). Beispiel-Lehre: Restart-Allowlist lebt NUR in
|
||||
`services.maintenance` (System-Dienste via `sudo -n`, User-Dienste via `systemctl --user`);
|
||||
keine zweite Allowlist in einem Router duplizieren.
|
||||
- **Gate vor Commit:** `python -m py_compile <geänderte .py>` muss durchlaufen.
|
||||
- Wartung ist **sudo-frei** gedacht (systemctl --user). Wo doch sudo nötig ist (llama-swap =
|
||||
System-Dienst), sauber über die NOPASSWD-Whitelist / `password_required`-Rückgabe, nie hart failen.
|
||||
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
||||
|
||||
## Grenzen (Verdikt, eingehalten)
|
||||
- **Hermes-Quellcode nie selbst patchen** (Fork verboten). Config/Deps ja, Code-Umbau nein.
|
||||
- **Security-Config** (approvals, Tokens, ufw, command_allowlist) **nie ohne explizites User-Ja.**
|
||||
- Alles reversibel halten: Branch + Backup + Pin.
|
||||
|
||||
## Gitea
|
||||
Remote = `Hitonabi/mission-control-v2`. Auth ist flatterhaft → **Push mit Retry**, nur über
|
||||
PowerShell/GCM. Neue Repos per API anlegen. Kein GitHub.
|
||||
# AGENTS.md — Mission Control 2.0
|
||||
|
||||
Projekt-Geschmack für Coding-Agenten (Kilo Code, Claude Code lesen diese Datei).
|
||||
Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `README.md`, `docs/`.
|
||||
|
||||
## Was das ist
|
||||
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
||||
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
|
||||
**Hermes-Agent (das autonome Gehirn "Lucy")**. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
||||
|
||||
**Hardware-Spezifikationen der Box (WICHTIG für alle Agenten):**
|
||||
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
||||
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
||||
**Gedächtnis & Dienste:** Hermes ist das autonome Agenten-Gehirn („Lucy") **und die alleinige Gedächtnis-Wahrheit** — es führt sein Gedächtnis selbst. Der frühere Sidecar auf `:8765` samt Memory-MCP-Server und MC2-Memory-Plugin wurde am 07.08.2026 abgelöst und am 27.08.2026 restlos ausgebaut (`docs/wissen/VERDIKTE.md`): MC2 hat **keine** `/api/memory`-Routen mehr, nichts darf mehr dorthin greifen. Governor, Zed-Workflows und alte Mittelschichten sind komplett gelöscht.
|
||||
|
||||
## Sprache (nicht verhandelbar)
|
||||
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
||||
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
|
||||
- **Hermes' INTERNE System-Prompts bleiben Englisch** (fremde Software, wir forken sie nicht).
|
||||
Antwort-Sprache ≠ Prompt-Sprache — Deutsch kommt aus SOUL.md, nicht aus den Tool-Prompts.
|
||||
|
||||
## Build & Deploy (die häufigste Falle)
|
||||
- **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die
|
||||
gebauten Assets direkt aus. Nach jeder Frontend-Änderung: `cd frontend && npm run build`, dann
|
||||
**das neue `frontend/dist` mit-committen**. Vergessen = Box zeigt alten Stand.
|
||||
- **Deploy macht `git reset --hard origin/main`** (`deploy/deploy.sh`). Heißt: **`main` muss vor
|
||||
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
|
||||
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
||||
|
||||
## Agentic IDE & Vibe Coding
|
||||
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
|
||||
- Es gibt keinen Zed-Workflow, keine OpenCode-CLI-Mittelschicht und keinen Governor mehr.
|
||||
- Der Agent in OpenCode Desktop nutzt via MCP (`.agents/mcp_config.json`) die API der Box (`:9001/v1`), um autonom Projekte zu bauen.
|
||||
- **Mix-Ansatz beim Testen:** Der Agent testet lokal. Vor dem Push muss er prüfen, ob das Gitea-`VERIFY`-Skript fehlerfrei durchläuft.
|
||||
|
||||
## Zeit & Umgebung
|
||||
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
||||
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
||||
(siehe `backend/services/reminders.py`).
|
||||
|
||||
## Backend-Konventionen
|
||||
- Router unter `backend/routers/` (`APIRouter(prefix="/api")`), Logik in `backend/services/`
|
||||
(Single Source of Truth — Router bleiben dünn). Beispiel-Lehre: Restart-Allowlist lebt NUR in
|
||||
`services.maintenance` (System-Dienste via `sudo -n`, User-Dienste via `systemctl --user`);
|
||||
keine zweite Allowlist in einem Router duplizieren.
|
||||
- **Gate vor Commit:** `python -m py_compile <geänderte .py>` muss durchlaufen.
|
||||
- Wartung ist **sudo-frei** gedacht (systemctl --user). Wo doch sudo nötig ist (llama-swap =
|
||||
System-Dienst), sauber über die NOPASSWD-Whitelist / `password_required`-Rückgabe, nie hart failen.
|
||||
- ‼️ **Die feingranularen sudoers.d-Regeln sind faktisch wirkungslos.** In `/etc/sudoers` steht
|
||||
`hitonabi ALL=(ALL) NOPASSWD: ALL` — der Nutzer, unter dem alle MC2-Dienste laufen, darf ohnehin
|
||||
alles passwortlos. `sudoers.d/mc2-autonomie` und `sudoers.d/mission-control` dokumentieren also,
|
||||
was gebraucht *würde*, schränken aber nichts ein. Das ist eine bewusste Entscheidung für die
|
||||
Single-User-Appliance; verlasse dich beim Bauen nicht darauf, dass eine Whitelist dich bremst.
|
||||
Wer das wirklich härten will, kommt um einen eigenen Service-User nicht herum — die Pauschalzeile
|
||||
einfach zu ziehen, bricht OS-Update, Config-Sync und den wöchentlichen Auto-Neustart still.
|
||||
(Geprüft 27.08.2026; die doppelte Zeile wurde damals entfernt, Sicherung `/root/sudoers.bak-*`.)
|
||||
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
||||
|
||||
## Grenzen (Verdikt, eingehalten)
|
||||
- **Hermes-Quellcode nie selbst patchen** (Fork verboten). Config/Deps ja, Code-Umbau nein.
|
||||
- **Security-Config** (approvals, Tokens, ufw, command_allowlist) **nie ohne explizites User-Ja.**
|
||||
- Alles reversibel halten: Branch + Backup + Pin.
|
||||
|
||||
## Gitea
|
||||
Remote = `Hitonabi/mission-control-v2`. Auth ist flatterhaft → **Push mit Retry**, nur über
|
||||
PowerShell/GCM. Neue Repos per API anlegen. Kein GitHub.
|
||||
|
||||
@@ -11,9 +11,9 @@ prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
|
||||
|
||||
| Bahn | Was sie tut | Wo |
|
||||
|---|---|---|
|
||||
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
|
||||
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue | MC2 `:9001` |
|
||||
| **Coding** | Agentic IDE (z.B. OpenCode Desktop) auf Dev-PC via lokaler API + MCP | 100% lokal |
|
||||
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
|
||||
| **Lucy** | Innere Stimme am PC (HUD, kein Avatar) und auf Telegram — eine Stimme, ein Hirn | eigenes Repo `Hitonabi/lucy` |
|
||||
|
||||
Lucy holt ihr Hirn direkt über das MC2-Gateway (`:9010/v1`) mit nativer Bildweiche und Voice-Streaming.
|
||||
|
||||
@@ -30,7 +30,7 @@ Lucy holt ihr Hirn direkt über das MC2-Gateway (`:9010/v1`) mit nativer Bildwei
|
||||
|
||||
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` ·
|
||||
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
|
||||
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
|
||||
Auto-Update und Self-Smoke. `llama-swap` läuft als System-Unit.
|
||||
|
||||
## Die Bau-Pipeline
|
||||
|
||||
@@ -49,29 +49,33 @@ Wächtern geboren**:
|
||||
|
||||
## Modelle & Rollen
|
||||
|
||||
Gemessen auf der Box (Stand: August 2026, Vulkan b10502):
|
||||
Gemessen auf der Box (Stand: 27.08.2026, Vulkan b10653):
|
||||
|
||||
| Rolle | Modell | Tempo | Aufgabe |
|
||||
|---|---|---|---|
|
||||
| `coder` | Qwen3-Coder-Next (80B-A3B) | **51,5 t/s** · Prefill **754 t/s** | Plant und baut — Kopf der Coding-Mannschaft (131k Kontext) |
|
||||
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6–90 t/s** | Lucys Hirn **und** Sucher-Subagent. Immer warm |
|
||||
| `debugger` / `doctor` | Muse-Glimmer-30B | **40–50 t/s** (DFlash) | Runtime-Debugger & Fehler-Diagnostiker (Multimodal) |
|
||||
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265–318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen, 16k Deckel) |
|
||||
| `coder` | Qwen3.8-27B (dicht, 27B) | **12,7 t/s** | Plant und baut — der Haupt-Coder. **131k Kontext** (ganzer Slot), multimodal |
|
||||
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6–90 t/s** | Lucys Hirn **und** Sucher-Subagent. Immer warm, 65k/Slot |
|
||||
| `debugger` | Muse-Glimmer-30B | **40–50 t/s** (DFlash) | Runtime-Debugger & Fehler-Diagnostiker (multimodal) |
|
||||
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen (On-Demand) |
|
||||
| `scout` | GLM-4.6V-Flash | auf Abruf | Schneller Vision- und Tool-Allrounder |
|
||||
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30 Uhr). **Nie tagsüber** — verdrängt das warme Set |
|
||||
| `dense-planer` | Qwen3.8-27B | **12,7 t/s** (On-Demand) | Dichtes 27B-Modell für tiefes Reasoning & 262k Kontext |
|
||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Vektorsuche + Feinsortierung |
|
||||
|
||||
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
||||
auf dieser bandbreitenbegrenzten Box bricht sein Prefill mit wachsendem Kontext ein (bei 32k
|
||||
gemessene 63 t/s ≈ **9 Minuten nur zum Lesen**). Mit dem 16k-Deckel bleibt der schlimmste Fall
|
||||
je Review unter einer Minute. Deshalb ist er der **Gegenleser für Etappen**, nicht der Coder —
|
||||
als Coder ist er auf dieser Box disqualifiziert (siehe VERDIKTE).
|
||||
> Sieben Rollen, mehr nicht. Frühere Fassungen listeten hier auch `kritiker`, `scout` und
|
||||
> `dense-planer` — die Modell-Konsolidierung vom 19.08. hat sie entfernt (ein Coder, ein
|
||||
> Agent-Hirn). Der dichte 27B ist seither nicht mehr „Planer" neben dem Coder, sondern **ist**
|
||||
> der Coder. Details: `docs/wissen/STACK.md`.
|
||||
|
||||
‼️ **Dichte Modelle sind auf dieser Box bandbreitengebunden.** Der Prefill bricht mit wachsendem
|
||||
Kontext ein — beim früheren Kritiker (Devstral, dicht) waren bei 32k gemessene 63 t/s ≈ **9 Minuten
|
||||
nur zum Lesen**, weshalb er hart auf 16k gedeckelt war. Das gilt weiter für jedes dichte Modell:
|
||||
`coder` (Qwen3.8-27B) liefert ~12,7 t/s gegen ~90 t/s des MoE-Hirns — **kein Konfigfehler, sondern
|
||||
das 215-GB/s-Limit der Plattform.** Der Kritiker selbst ist seit dem 19.08. nicht mehr im Stack.
|
||||
|
||||
‼️ **Speicher-Regel:** `Warm-Set + größtes On-Demand-Modell ≤ ~115 GB`. Die ko-residente Gruppe
|
||||
(`groups.brains` in der llama-swap-Config) muss **`persistent: false`** sein — sonst räumt
|
||||
llama-swap vor einem großen Modell nicht ab und der Kernel schießt Prozesse ab. Details und
|
||||
(`groups.brains`) steht auf **`persistent: true`** (Verdrängungsschutz; live gegengeprüft
|
||||
27.08.2026). Wichtig ist nicht der Schalter, sondern: **alles, was gleichzeitig warm sein muss,
|
||||
gehört in DIESELBE Gruppe** — zwei Gruppen verdrängen sich gegenseitig, und `persistent` schützt
|
||||
nicht davor (gemessen 25.07.). Details und
|
||||
Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
|
||||
|
||||
## Qualitäts-Tore
|
||||
@@ -94,7 +98,7 @@ Kontext-Komprimierung löscht still die Regeln mit.
|
||||
## API (Auswahl)
|
||||
|
||||
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
|
||||
memory/* · agent/* · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
|
||||
memory/* · agent/* · ideen/* · lucy/stimme/* · chronik · eigenleben · wissen ·
|
||||
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
|
||||
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
|
||||
|
||||
@@ -138,7 +142,6 @@ Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `n
|
||||
| [`docs/BEDIENUNG.md`](docs/BEDIENUNG.md) | Wie man MC2 benutzt |
|
||||
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) · [`docs/DISASTER_RECOVERY.md`](docs/DISASTER_RECOVERY.md) | Betrieb, Störungen, Wiederherstellung |
|
||||
| [`docs/HERMES_SETUP.md`](docs/HERMES_SETUP.md) | Hermes-Agent, Profile, Crons |
|
||||
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
|
||||
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
|
||||
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
|
||||
|
||||
|
||||
+49
-168
@@ -1,176 +1,57 @@
|
||||
# Savepoint — Mem0-Ausbau (Referenzaufgabe)
|
||||
# Savepoint — Mission Control 2.0 (MC2)
|
||||
|
||||
**Stand:** 2026-08-22, Worktree `referenz/mem0-ausbau` in `F:\Coding Stuff\mc2-referenz`.
|
||||
Branch wurde aus `d508a84` (A2's gesicherter Stand) erstellt und enthält dessen 5 bearbeitete Dateien.
|
||||
_Stand: 2026-08-20, nach der Aufräum-Runde. Alle Zahlen hier sind **auf der Box gemessen**, nicht
|
||||
aus Doku übernommen._
|
||||
|
||||
## Auftrag
|
||||
`docs/aufgaben/referenzaufgabe-mem0-ausbau.md`: Mem0-Reste vollständig entfernen, ohne MC2/Frontend/Deployment zu brechen.
|
||||
- Messlatte: `ruff` clean, Backend `import app` sauber, `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` = 0 Dateien, `mem0_service/` entfernt, Box-Smoke/Health so weit wie möglich grün.
|
||||
- **Prüfstein:** API-Felder wie `mem0_ms` nicht einfach löschen — Frontend mit anpassen oder Feld sauber aus beiden Seiten entfernen.
|
||||
- Arbeitsweise: Datei für Datei, höchstens zwei vor der ersten Änderung. Nach jeder Änderung ein Satz.
|
||||
- Live-Deployment `~/mission-control-v2` ist TABU.
|
||||
## Was das ist (in einem Satz)
|
||||
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
||||
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
|
||||
Agenten-Status, Updates & Wartung — und dient als Steuerpult für die autonome KI („Lucy").
|
||||
|
||||
## Wichtig: A2-Vorgang (Commit d508a84 — bereits auf diesem Branch)
|
||||
Diese 5 Dateien wurden vom abgebrochenen Lauf A2 bereits bearbeitet und im Commit `d508a84` gesichert:
|
||||
1. `backend/config.py` — `MEMORY_DB` und `MC_MEM_DEDUPE_ENABLED` entfernt, `MEM0_SERVICE_URL`-Kommentar bereinigt
|
||||
2. `backend/routers/system.py` — `_mem0_reachable()` entfernt, `mem0_reachable` aus Systeminfo
|
||||
3. `backend/routers/voice.py` — `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||
4. `backend/routers/zeitmaschine.py` — `Mem0` in Docstrings/Kommentaren → `Gedächtnis`
|
||||
5. `backend/services/sentry.py` — `MC_MEM0_DASHBOARD_URL` in Docstring entfernt
|
||||
## Architektur (Kurzform — Details in AGENTS.md/docs/)
|
||||
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
|
||||
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
||||
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
|
||||
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
|
||||
- **Modell-Router** `:8080` (llama-swap v250, Vulkan/RADV Engine b10502).
|
||||
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4) + `hermes dashboard` auf `:9119` (nur Loopback).
|
||||
- **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.
|
||||
|
||||
`git status` war nach d508a84 clean.
|
||||
## Modelle live (gemessen 20.08.2026)
|
||||
| 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 |
|
||||
|
||||
## 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
|
||||
Warm-Gruppe `brains` = embed + reranker + Qwen3.6-35B-A3B. Warm-Wächter weckt nur `fast`.
|
||||
|
||||
## Erledigt in dieser Sitzung
|
||||
**Hintergrund zum Tempo:** Strix Halo liefert ~215 GB/s Speicherbandbreite. Ein *dichtes* Modell
|
||||
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.
|
||||
|
||||
### 1. `backend/steward.py`
|
||||
- Toter Import `from services import memory as memory_svc` entfernt
|
||||
- Toter `memory_svc.AUTO_DEDUPE_ENABLED`-Block entfernt
|
||||
- Docstring: `Mem0-Auto-Dedupe` → `Re-Warm-Wächter und Health-Wächter (sentry)`
|
||||
- Docstring: `memory.auto_dedupe_loop — Gedächtnis-Dubletten` entfernt
|
||||
- Docstring: `MC_MEM_DEDUPE_ENABLED` entfernt
|
||||
- Verifiziert: `memory.py` wurde in Kahlschlag gelöscht, `steward.py` hatte toten Import
|
||||
## Aufgeräumt am 20.08.2026
|
||||
- CI-Ampel wieder **grün** (Lauf #77) — zwei ungenutzte `MEM0_SERVICE_URL`-Importe entfernt.
|
||||
- Kaputter systemd-Timer `mc2-morgen-digest` abgeschaltet (der Hermes-Cron macht die Arbeit).
|
||||
- `mcp-stderr.log` (27 MB, 238k Ping-Zeilen) geleert.
|
||||
- Config-Drift aufgelöst: Repo == `/etc/llama-swap/config.yaml`. Warm-Set-Idee geparkt auf
|
||||
Branch `parked/warm-set-5d93822`.
|
||||
- Entfernt: Governor-Reste, OpenCode-/aider-Reste (853 MB), 61 alte Backup-Dateien,
|
||||
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).
|
||||
|
||||
### 2. `backend/services/voice_metrics.py` (alle 9 Treffer bereinigt)
|
||||
- **Docstring (3x):** `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||
- **Line 10:** `Mem0-Retrieve` im Best-effort-Kommentar → `Gedächtnis-Retrieve`
|
||||
- **Line 39:** `Mem0-Retrieve` im docstring → `Gedächtnis-Retrieve`
|
||||
- **Line 56:** `Geparkten Mem0-Retrieve` → `Geparkten Gedächtnis-Retrieve`
|
||||
- **Line 106:** `Unter-Detail: mem0 (Teil VON hirn...` → `Unter-Detail: memory (Teil VON hirn...`
|
||||
- **Line 116:** `Agent + Mem0 + TTFT` → `Agent + Gedächtnis + TTFT`
|
||||
- **Line 133:** `Agent-Overhead + Mem0 + LLM-TTFT` → `Agent-Overhead + Gedächtnis + LLM-TTFT`
|
||||
- **Line 141:** `mem0 = _take_retrieve(...)` → `memory = _take_retrieve(...)` (starker Kommentar)
|
||||
- **Line 154:** `"mem0_ms"` → `"memory_ms"` (API-Feld im JSON-Rückgabewort)
|
||||
|
||||
### 3. `frontend/src/lib/api.ts` (3 Treffer)
|
||||
- **Line 446:** `mem0 = Unter-Detail INNERHALB von hirn` → `memory = Unter-Detail INNERHALB von hirn`
|
||||
- **Line 455:** `Agent + Mem0 + LLM-TTFT` → `Agent + Gedächtnis + LLM-TTFT`
|
||||
- **Line 457:** `mem0_ms: number | null // ... (Mem0-Retrieve)` → `memory_ms: number | null // ... (Gedächtnis-Retrieve)`
|
||||
|
||||
### 4. `frontend/src/components/dashboard/LatencyCard.tsx` (4 Treffer)
|
||||
- **Line 5:** `Mem0 ist ein Unter-Detail` → `Gedächtnis ist ein Unter-Detail`
|
||||
- **Line 42:** `t.mem0_ms` (2x) → `t.memory_ms` (2x)
|
||||
- **Line 106:** `latest.mem0_ms` (2x) → `latest.memory_ms` (2x)
|
||||
- **Line 131:** `Agent + Mem0-Suche` → `Agent + Gedächtnis-Suche`
|
||||
|
||||
### 5. `frontend/src/views/GuideView.tsx` (1 Treffer, kompletter Paragraph)
|
||||
- Stack-Paragraph (Card 8 "Gedächtnis & RAG"): "Dein Gedächtnis ist Mem0-basiert: auto-lernend..." umgeschrieben zu "Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn..."
|
||||
- Gotcha-Zeile (Mem0-extraktion) entfernt
|
||||
- Begründung: Gedächtnis-Tab existiert nicht mehr (Kahlschlag), Mem0 wurde durch Hermes-Nativ abgelöst
|
||||
|
||||
### 6. `backend/services/backup.py` (1 Treffer)
|
||||
- Docstring: `Full state backup (mem0 + ...)` → `Full state backup (...)`
|
||||
|
||||
### 7. `deploy/backup.sh` (8 Treffer)
|
||||
- **Line 6-7:** Kommentar `mem0 (Chroma + history.db)` aus Inhalt entfernt
|
||||
- **Line 23:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — ganze Zeile gelöscht
|
||||
- **Line 30:** `mkdir -p "$STAGE/mem0"` — aus mkdir entfernt
|
||||
- **Line 32:** `[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true` — ganze Zeile gelöscht
|
||||
- **Line 63:** Kommentar `mem0-venv ist uv-managed` → `voice-venv ist uv-managed`
|
||||
- **Line 71:** `freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"` — ganze Zeile gelöscht
|
||||
- **Line 82:** `$HOME/.mem0/venv/bin/python` aus for-Schleife entfernt
|
||||
- **Line 95:** MANIFEST: `mem0 (chroma + history.db)` aus Inhalt entfernt
|
||||
|
||||
### 8. `mcp/mcp_mc.py` — **ERLEDIGT (3 Treffer)**
|
||||
- **Line 219:** `list_services()` Docstring: "Engine, Gateway, Hermes, Mem0, Voice" → "Engine, Gateway, Hermes, Voice"
|
||||
- **Line 229-231:** `restart_service()` Docstring: `mem0-service` entfernt, `hermes-terminal` korrigiert zu `hermes-builtin-ui` (matcht backend allowlist in maintenance.py)
|
||||
- **Line 238:** `backup_now()` Docstring: "mem0 + Configs + Secrets" → "Configs + Secrets"
|
||||
- Verifiziert: `rg -i mem0 mcp/mcp_mc.py` = 0 Treffer, `py_compile mcp/mcp_mc.py` OK
|
||||
- **Achtung:** Der mc2-memory-Plugin (`hermes/plugins/mc2-memory/__init__.py`) ruft noch `/api/memory` und `/api/memory/learn` auf. Der Kahlschlag-Commit hat `backend/routers/memory.py` gelöscht → dieser API-Pfad existiert nicht mehr. Der Plugin degraded silently (prefetch="", learn fail). **Entscheidung:** Nicht bearbeiten, Plugin ist im `hermes/plugins/`-Ordner, nicht in der Messlatte.
|
||||
|
||||
### 9. `deploy/agent-hooks/box-steckbrief-inject.sh`
|
||||
- Treffer: "mem0-Gedaechtnis :8765" in einer langen Agent-Injektionszeile (Zeile 31)
|
||||
- Nur Text → mem0-Gedaechtnis :8765 entfernen
|
||||
|
||||
### 10. `deploy/autoupdate.sh`
|
||||
- Treffer: "Gehirn-Check (Mem0, Tools, Voice)" → "Gehirn-Check (Tools, Voice)"
|
||||
|
||||
### 11. `deploy/hermes-postcheck.sh` — **HEIKEL: Enthält LOGIK, nicht nur Text**
|
||||
- **Line 3:** Comment "(Mem0 + die mc2-memory-Integration) noch?" → muss aktualisiert werden
|
||||
- **Line 8:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable für Sidecar-Check (Zeilen 14-18)
|
||||
- **Zeilen 14-18:** curl-Check auf `:8765/health` → **wird immer FAILn**, da mem0_service stillgelegt
|
||||
- **Zeilen 20-24:** curl-Check auf `/api/memory` → **wird immer FAILn**, da memory.py in Kahlschlag gelöscht
|
||||
- **Zeilen 26-31:** grep-Check für `memory.provider=mc2-memory` in config.yaml → das ist **neues** Hermes-nativ-Memory, kein Mem0-Rest. **NICHT entfernen** (gehört zur aktuellen Architektur).
|
||||
- **Zeilen 52-58:** Plugin-Check für `mc2-memory` → ebenfalls **neue** Architektur, kein Mem0-Rest. **NICHT entfernen**.
|
||||
- **Entscheidung:** Zeilen 8, 14-18, 20-24 entfernen (toter Sidecar + toter /api/memory-Endpunkt). Zeilen 26-31 und 52-58 behalten (gehören zur neuen Hermes-native Memory).
|
||||
|
||||
### 12. `deploy/restore.sh` — **LOGIK: Gegenstück zu backup.sh**
|
||||
- **Line 18:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — Variable
|
||||
- **Line 22:** `SERVICES="mem0-service mission-control-2 hermes-gateway"` → "mem0-service" entfernen
|
||||
- **Line 101:** dry-run echo "mem0 → $MEM0_DIR" — mem0-Zeile entfernen
|
||||
- **Line 122:** `if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir ...; cp ...; fi` — ganze Zeile entfernen
|
||||
- Da backup.sh kein `$STAGE/mem0` mehr erstellt, ist diese Logik tot (würde nie feuern). Entfernen.
|
||||
|
||||
### 13. `deploy/stack-postcheck.sh` — **LOGIK: Enthält toten curl-Check**
|
||||
- **Line 6:** Comment "prüft das Gehirn/Mem0" → "prüft das Gehirn"
|
||||
- **Line 12:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable
|
||||
- **Line 14:** Comment "Embedding-Modell (Mem0/Gedächtnis)" → "Embedding-Modell (Gedächtnis)"
|
||||
- **Line 51:** Comment "Embedding-Modell (für Mem0/Gedächtnis)" → "Embedding-Modell (für Gedächtnis)"
|
||||
- **Line 58:** "(Mem0/Gedächtnis betroffen)" → "(Gedächtnis betroffen)"
|
||||
- **Zeilen 88-93:** curl-Check auf `MEM0_URL/health` → **wird immer FAILn**, da Sidecar tot. **Ganze Block entfernen.**
|
||||
- Die Embedding-Checks (Zeilen 51-59) bleiben — sie prüfen das Embedding-Modell, das auch für die neue Memory-Architektur relevant ist.
|
||||
|
||||
### 14. `deploy/warmup.sh`
|
||||
- Treffer: "für Mem0/Gedächtnis" → "für Gedächtnis" (nur Text)
|
||||
|
||||
### 15. `deploy/venv-audit.sh`
|
||||
- **Line 2:** Comment "(Mem0, Voice) altern" → "(Voice) altern"
|
||||
- **Line 12:** `VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")` → mem0-Entry entfernen
|
||||
|
||||
### 16. Ordner `mem0_service/` löschen
|
||||
- Enthält: `app.py`, `migrate.py`, `smoke_test.py`, `requirements.txt`
|
||||
- Toter Sidecar: Port `:8765` antwortet nicht, nicht systemd-registered
|
||||
|
||||
## Noch nicht touchen (out-of-scope / unklar)
|
||||
|
||||
### `voice_service/app.py` + `voice_service/install.sh`
|
||||
- Enthalten "mem0" — gehören zur voice_service, nicht zur Messlatte
|
||||
- voice_service.service wurde in Kahlschlag gelöscht, aber voice_service/ Ordner ist noch vorhanden
|
||||
- **Entscheidung:** Nicht bearbeiten, da nicht in `backend/`, `mcp/`, `deploy/` Messlatte
|
||||
|
||||
### `hermes/plugins/mc2-memory/` (2 Dateien)
|
||||
- `__init__.py`, `plugin.yaml`
|
||||
- Rufen `/api/memory` auf → existiert nicht mehr, degradieren gracefully
|
||||
- Nicht in Messlatte (`deploy/` mit `.py/.sh` — YAML zählt nicht, Python-Plugin ist extern)
|
||||
- **Entscheidung:** Nicht bearbeiten (Hermes-Plugin, nicht Teil von MC2)
|
||||
|
||||
### `deploy/mission-control-2.service` + `deploy/mc2-backup.service`
|
||||
- systemd Service-Dateien (keine `.py` oder `.sh`)
|
||||
- Nicht in Messlatte, aber könnten Mem0-Referenzen haben
|
||||
- **Entscheidung:** Prüfen, ob sie Mem0 nennen; wenn ja, Text anpassen
|
||||
|
||||
### `deploy/llama-swap.config.yaml`
|
||||
- YAML, nicht in Messlatte
|
||||
|
||||
### `frontend/dist/`
|
||||
- Muss nach Änderungen neu gebaut werden (`npm run build`)
|
||||
- **NACHDEM alle .ts/.tsx Dateien bearbeitet sind**
|
||||
|
||||
## Bekannte Fallstricke
|
||||
|
||||
1. **Prüfstein (Frontend-Konsistenz):** `mem0_ms` war ein API-Feld, das das Frontend las. Wurde zu `memory_ms` umbenannt + Frontend angepasst. **Nicht einfach löschen.**
|
||||
2. **Restore vs. Backup:** `restore.sh` ist das Gegenstück zu `backup.sh`. Wenn backup.sh mem0 weglässt, darf restore.sh nicht versuchen, mem0 zu restaurieren. **Sorgfältig prüfen!**
|
||||
3. **`head` funktioniert in PowerShell nicht:** Muss `Select-Object -First N` verwenden.
|
||||
4. **Globs für native Befehle:** `rg` mit Globs muss gequoted werden, sonst spaltet PowerShell falsch.
|
||||
5. **`frontend/dist` ist im git:** Nach Frontend-Änderungen muss `npm run build` laufen und das neue `dist` committet werden.
|
||||
6. **`/api/memory` existiert nicht mehr** (Kahlschlag), aber `frontend/src/lib/queries.ts` ruft es noch via `useMemory()`: `api<Memory[]>(`/api/memory`...`. Das ist ein **vorher bestehendes Problem** (nicht eingeführt in dieser Runde).
|
||||
|
||||
## Nächster Schritt
|
||||
1. **mcp/mcp_mc.py committen** (bereits bearbeitet)
|
||||
2. `deploy/agent-hooks/box-steckbrief-inject.sh` bearbeiten (1 Treffer, Text)
|
||||
3. `deploy/autoupdate.sh`, `deploy/warmup.sh` bearbeiten (je 1 Treffer, Text)
|
||||
4. `deploy/venv-audit.sh` bearbeiten (2 Treffer, Text + Venv-Liste)
|
||||
5. `deploy/stack-postcheck.sh` bearbeiten (6 Treffer, 1 Block-Entfernung, 4 Kommentare)
|
||||
6. `deploy/restore.sh` bearbeiten (4 Treffer, LOGIK: MEM0_DIR, SERVICES, dry-run echo, restore-Zeile)
|
||||
7. `deploy/hermes-postcheck.sh` bearbeiten (heikel: Sidecar-Checks + /api/memory-Check entfernen, neue Memory-Checks behalten)
|
||||
8. `mem0_service/` Ordner löschen
|
||||
9. `ruff` + `import app` prüfen
|
||||
10. `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` → muss 0 ergeben
|
||||
11. `frontend/dist` neu bauen (Box: `cd frontend && npm run build`)
|
||||
## Offen
|
||||
- `mem0_service/` + `_mem0_reachable()` in `backend/routers/system.py` ausbauen (toter Port).
|
||||
- `~/.hermes/scripts/fremdblick.sh` ist **unversioniert** — gehört ins Repo.
|
||||
- MCP-Log dauerhaft deckeln (Hermes' `max_size_mb` greift für gekapertes stderr nicht).
|
||||
- Verschwundene Wächter: Ampel-Wächter, Prüfstand, Stack-Rundumschlag.
|
||||
- Coder-Tempo: Entwurfsmodelle für spekulatives Dekodieren liegen ungenutzt unter
|
||||
`/srv/models/Qwen3.8-27B-DFlash2-GGUF/`.
|
||||
|
||||
@@ -21,7 +21,6 @@ from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from routers import (
|
||||
agent,
|
||||
auftragsbuch,
|
||||
chronik,
|
||||
connect,
|
||||
console,
|
||||
@@ -126,7 +125,6 @@ if V1_UPSTREAM:
|
||||
else:
|
||||
app.include_router(gateway_proxy.router) # OpenAI-kompatibler /v1-Gateway (model:auto)
|
||||
app.include_router(maintenance.router)
|
||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
||||
|
||||
+144
-129
@@ -1,129 +1,144 @@
|
||||
"""
|
||||
Zentrale Konfiguration für Mission Control 2.0.
|
||||
|
||||
Eine Quelle der Wahrheit für Pfade, URLs und Defaults — alles über Env-Vars
|
||||
überschreibbar. Bewusst schlank: MC 2.0 ist ein Glue-Cockpit, das vorhandene
|
||||
Dienste (llama-swap, LiteLLM-Gateway, Hermes) steuert, statt sie nachzubauen.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
# --- Engine (llama-swap) -----------------------------------------------------
|
||||
LLAMA_SWAP_URL = os.environ.get("MC_LLAMA_SWAP_URL", "http://127.0.0.1:8080").rstrip("/")
|
||||
CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/etc/llama-swap/config.yaml"))
|
||||
MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
||||
# Cache der Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace).
|
||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||
# macht llama-server ohnehin automatisch pro Slot (KV-Reuse).
|
||||
_DEFAULT_CMD_TEMPLATE = (
|
||||
"llama-server -m {model} --host 127.0.0.1 --port ${PORT} "
|
||||
"-c {ctx} -ngl 999 -fa on --no-mmap"
|
||||
)
|
||||
CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
|
||||
if "{model}" not in CMD_TEMPLATE:
|
||||
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
|
||||
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
||||
# Verzeichnis mit Draft-Modellen für Speculative Decoding. Beim Hinzufügen eines
|
||||
# fast/coder-Modells wird hieraus automatisch ein **vocab-kompatibler** Draft gewählt
|
||||
# (Vocab-Check via services.gguf_meta; ein inkompatibler Draft lässt llama.cpp scheitern).
|
||||
DRAFTS_DIR = Path(os.environ.get("MC_DRAFTS_DIR", str(MODELS_DIR / "drafts")))
|
||||
# Optionaler expliziter Default-Draft (leer = Auto-Erkennung aus DRAFTS_DIR). Wird nur
|
||||
# verwendet, wenn er zum Ziel-Modell vocab-kompatibel ist. (Früher fix qwen2.5 → entfernt,
|
||||
# weil das mit neueren Vocabs wie Qwen3.6 inkompatibel ist und Spec stillschweigend brach.)
|
||||
SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "")
|
||||
# Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich
|
||||
# zu --spec-draft-model, sonst ist Spec inaktiv).
|
||||
SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple")
|
||||
# MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen
|
||||
# MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit
|
||||
# `--model-draft <mtp.gguf> --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT
|
||||
# --spec-draft-model/draft-simple). 1,5–2× Durchsatz bei null Qualitätsverlust.
|
||||
SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4"))
|
||||
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
|
||||
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
||||
|
||||
# --- Routing-Gateway (builtin in MC2, model: auto) ---------------------------
|
||||
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
|
||||
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
|
||||
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
|
||||
# Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B.
|
||||
# http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad
|
||||
# und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes
|
||||
# Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback.
|
||||
V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
||||
|
||||
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
|
||||
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
|
||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||
# (Tools + geteiltes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||
def _read_hermes_env(key: str) -> str:
|
||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||
try:
|
||||
env_path = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) / ".env"
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith(f"{key}="):
|
||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
HERMES_API_KEY = (
|
||||
os.environ.get("HERMES_API_KEY")
|
||||
or os.environ.get("API_SERVER_KEY")
|
||||
or _read_hermes_env("API_SERVER_KEY")
|
||||
)
|
||||
# Modellfeld im OpenAI-Request; die api_server-Plattform nutzt ihr konfiguriertes Hirn,
|
||||
# das Feld ist i.d.R. kosmetisch. Override via Env, falls die Plattform strikt prüft.
|
||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||
|
||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv). MC2 proxyt nach außen.
|
||||
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
||||
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
|
||||
# KEINE eigene Firewall-Freigabe (Port 7682 ist von außen dicht) und keinen sudo-Eingriff.
|
||||
# Direkter, SSH-artiger Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie das Dashboard.
|
||||
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
|
||||
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
||||
BOX_CONSOLE_PATH = "/console/"
|
||||
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
|
||||
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). Bindet NUR an Loopback
|
||||
# (127.0.0.1:9119, kein Login — LAN-Trust wie Terminal/Konsole) und wird von MC2 same-origin unter
|
||||
# /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle
|
||||
# SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix
|
||||
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
|
||||
HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/")
|
||||
HERMES_BUILTIN_UI_PATH = "/hermes-ui/"
|
||||
# GitHub-Repo für Update-Checks.
|
||||
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
||||
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
|
||||
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
|
||||
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.98:7777").rstrip("/")
|
||||
|
||||
# --- Server ------------------------------------------------------------------
|
||||
HOST = os.environ.get("MC_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("MC_PORT", "9000"))
|
||||
# Gebautes React-Frontend (frontend/dist). In Prod liefert FastAPI es statisch aus;
|
||||
# im Dev läuft der Vite-Dev-Server separat und proxyt /api hierher.
|
||||
FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist")))
|
||||
|
||||
# Version (Phase 0 — Greenfield-Skeleton).
|
||||
VERSION = "2.0.0-w8"
|
||||
|
||||
# Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml).
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
"""
|
||||
Zentrale Konfiguration für Mission Control 2.0.
|
||||
|
||||
Eine Quelle der Wahrheit für Pfade, URLs und Defaults — alles über Env-Vars
|
||||
überschreibbar. Bewusst schlank: MC 2.0 ist ein Glue-Cockpit, das vorhandene
|
||||
Dienste (llama-swap, LiteLLM-Gateway, Hermes) steuert, statt sie nachzubauen.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ruamel.yaml import YAML
|
||||
|
||||
# --- Engine (llama-swap) -----------------------------------------------------
|
||||
LLAMA_SWAP_URL = os.environ.get("MC_LLAMA_SWAP_URL", "http://127.0.0.1:8080").rstrip("/")
|
||||
CONFIG_PATH = Path(os.environ.get("MC_CONFIG_PATH", "/etc/llama-swap/config.yaml"))
|
||||
MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
||||
# Cache der Modell-Entdeckung ("aktuell beste Modelle", live von HuggingFace).
|
||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||
# Gedächtnis: MC2 hält KEINS mehr. Bis August 2026 lief hier erst eine eigene SQLite-DB,
|
||||
# dann ein semantischer Sidecar auf :8765 — beides ist abgelöst, der Port ist tot.
|
||||
# Einzige Gedächtnis-Wahrheit ist jetzt Hermes selbst (HERMES_API_URL, siehe unten).
|
||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||
# macht llama-server ohnehin automatisch pro Slot (KV-Reuse).
|
||||
# --load-mode none = das frühere --no-mmap; das alte Flag ist seit llama.cpp b10936 weg
|
||||
# ("invalid argument", jedes Modell stirbt beim Start — gelernt 13./17.09.2026).
|
||||
_DEFAULT_CMD_TEMPLATE = (
|
||||
"llama-server -m {model} --host 127.0.0.1 --port ${PORT} "
|
||||
"-c {ctx} -ngl 999 -fa on --load-mode none"
|
||||
)
|
||||
CMD_TEMPLATE = os.environ.get("MC_CMD_TEMPLATE", _DEFAULT_CMD_TEMPLATE)
|
||||
if "{model}" not in CMD_TEMPLATE:
|
||||
CMD_TEMPLATE = _DEFAULT_CMD_TEMPLATE
|
||||
DEFAULT_TTL = int(os.environ.get("MC_DEFAULT_TTL", "300"))
|
||||
# Verzeichnis mit Draft-Modellen für Speculative Decoding. Beim Hinzufügen eines
|
||||
# fast/coder-Modells wird hieraus automatisch ein **vocab-kompatibler** Draft gewählt
|
||||
# (Vocab-Check via services.gguf_meta; ein inkompatibler Draft lässt llama.cpp scheitern).
|
||||
DRAFTS_DIR = Path(os.environ.get("MC_DRAFTS_DIR", str(MODELS_DIR / "drafts")))
|
||||
# Optionaler expliziter Default-Draft (leer = Auto-Erkennung aus DRAFTS_DIR). Wird nur
|
||||
# verwendet, wenn er zum Ziel-Modell vocab-kompatibel ist. (Früher fix qwen2.5 → entfernt,
|
||||
# weil das mit neueren Vocabs wie Qwen3.6 inkompatibel ist und Spec stillschweigend brach.)
|
||||
SPEC_DRAFT_MODEL_PATH = os.environ.get("MC_SPEC_DRAFT_MODEL", "")
|
||||
# Speculative-Decoding-Typ (llama.cpp dieser Generation braucht --spec-type zusätzlich
|
||||
# zu --spec-draft-model, sonst ist Spec inaktiv).
|
||||
SPEC_TYPE = os.environ.get("MC_SPEC_TYPE", "draft-simple")
|
||||
# MTP-Speculative-Decoding (Multi-Token-Prediction): manche Modelle bringen einen eigenen
|
||||
# MTP-Kopf mit (z.B. gemma-4 → arch 'gemma4-assistant', Datei 'mtp-*.gguf'). Der wird mit
|
||||
# `--model-draft <mtp.gguf> --spec-type draft-mtp --spec-draft-n-max N` geladen (NICHT
|
||||
# --spec-draft-model/draft-simple). 1,5–2× Durchsatz bei null Qualitätsverlust.
|
||||
SPEC_DRAFT_N_MAX = int(os.environ.get("MC_SPEC_DRAFT_N_MAX", "4"))
|
||||
# Env für HuggingFace-Downloads: XET deaktivieren (Hänger bei ~6 MB, siehe v1-Gotcha).
|
||||
HF_DOWNLOAD_ENV = {"HF_HUB_DISABLE_XET": "1"}
|
||||
|
||||
# --- Routing-Gateway (builtin in MC2, model: auto) ---------------------------
|
||||
# MC2 IST der Gateway (services/gateway.py + routers/gateway_proxy.py). KEIN externer
|
||||
# LiteLLM-Dienst (scheitert auf Python 3.14). Daher keine Gateway-Config-Datei mehr.
|
||||
GATEWAY_URL = os.environ.get("MC_GATEWAY_URL", f"http://127.0.0.1:{os.environ.get('MC_PORT', '9000')}").rstrip("/")
|
||||
# Gateway-Auszug (UMBAU v3 P1): Ist MC_V1_UPSTREAM gesetzt (Unit-Env, z. B.
|
||||
# http://127.0.0.1:9010), bedient der eigenständige mc2-gateway-Prozess den /v1-Pfad
|
||||
# und MC2 reicht /v1 nur noch roh durch (routers/gateway_forward.py). Leer = altes
|
||||
# Verhalten, MC2 bedient /v1 selbst — die Zeile aus der Unit nehmen ist der Rollback.
|
||||
V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
||||
|
||||
# --- Hermes Agent (eigener Dienst auf der Box) -------------------------------
|
||||
# Gateway (OpenAI-API des Agenten) + interaktives Web-Terminal (ttyd → `hermes chat`).
|
||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||
# (Tools + eigenes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||
def _read_hermes_env(key: str) -> str:
|
||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||
try:
|
||||
env_path = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes"))) / ".env"
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith(f"{key}="):
|
||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
HERMES_API_KEY = (
|
||||
os.environ.get("HERMES_API_KEY")
|
||||
or os.environ.get("API_SERVER_KEY")
|
||||
or _read_hermes_env("API_SERVER_KEY")
|
||||
)
|
||||
# Modellfeld im OpenAI-Request; die api_server-Plattform nutzt ihr konfiguriertes Hirn,
|
||||
# das Feld ist i.d.R. kosmetisch. Override via Env, falls die Plattform strikt prüft.
|
||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||
|
||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv), weil Parakeet/Piper nicht ins 3.14-Backend-venv
|
||||
# passen. MC2 proxyt nach außen.
|
||||
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
||||
# --- Lucys Stimme (lucy-stimme.service, pocket-tts german_24l, Klon aus ref.mp3) -----------
|
||||
# Eigener User-Dienst in ~/.lucy-stimme (nur Loopback :8021). Spricht die Telegram-Sprachnachrichten
|
||||
# UND — seit dem Raphael-Umbau der Desktop-Lucy (04.09.2026) — auch den PC: MC2 reicht ihn unter
|
||||
# /api/lucy/stimme/* ins LAN (routers/voice.py). EINE Stimme für alle Türen. :8650 ist NICHT Lucy.
|
||||
LUCY_STIMME_URL = os.environ.get("MC_LUCY_STIMME_URL", "http://127.0.0.1:8021").rstrip("/")
|
||||
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
|
||||
# KEINE eigene Firewall-Freigabe (Port 7682 ist von außen dicht) und keinen sudo-Eingriff.
|
||||
# Direkter, SSH-artiger Zugriff, kein Passwort — gleiches LAN-Trust-Modell wie das Dashboard.
|
||||
BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0.1:7682").rstrip("/")
|
||||
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
||||
BOX_CONSOLE_PATH = "/console/"
|
||||
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
|
||||
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). ‼️ Sie bindet NICHT auf Loopback:
|
||||
# ein systemd-Drop-in überschreibt `--host 127.0.0.1` mit `0.0.0.0` und setzt dafür ein
|
||||
# Session-Token. Dass sie trotzdem nicht im LAN hängt, liegt allein an ufw (9119 ist nicht
|
||||
# freigegeben — am 27.08.2026 von einem zweiten Rechner aus gegengeprüft). Wer die Firewall
|
||||
# anfasst, öffnet damit auch diese Oberfläche. MC2 erreicht sie über Loopback und reicht sie
|
||||
# same-origin unter
|
||||
# /hermes-ui/ durchgereicht (routers/hermes_ui.py). Anders als das ttyd-Terminal ist es eine volle
|
||||
# SPA → das Bundle wird mit Vite-base=/hermes-ui/ gebaut, damit Assets/API/WS unter dem Präfix
|
||||
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
|
||||
HERMES_BUILTIN_UI_UPSTREAM = os.environ.get("MC_HERMES_BUILTIN_UI_UPSTREAM", "http://127.0.0.1:9119").rstrip("/")
|
||||
HERMES_BUILTIN_UI_PATH = "/hermes-ui/"
|
||||
# GitHub-Repo für Update-Checks.
|
||||
HERMES_AGENT_REPO = os.environ.get("MC_HERMES_AGENT_REPO", "NousResearch/hermes-agent")
|
||||
HERMES_HOME = Path(os.path.expanduser(os.environ.get("HERMES_HOME", "~/.hermes")))
|
||||
# PC Executor — läuft auf dem Windows-PC, erreichbar über LAN.
|
||||
PC_EXECUTOR_URL = os.environ.get("MC_PC_EXECUTOR_URL", "http://192.168.178.22:7777").rstrip("/") # PC-IP seit 09/2026 .22 (vorher .98)
|
||||
|
||||
# --- Server ------------------------------------------------------------------
|
||||
HOST = os.environ.get("MC_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("MC_PORT", "9000"))
|
||||
# Gebautes React-Frontend (frontend/dist). In Prod liefert FastAPI es statisch aus;
|
||||
# im Dev läuft der Vite-Dev-Server separat und proxyt /api hierher.
|
||||
FRONTEND_DIST = Path(os.environ.get("MC_FRONTEND_DIST", str(Path(__file__).resolve().parent.parent / "frontend" / "dist")))
|
||||
|
||||
# Version (Phase 0 — Greenfield-Skeleton).
|
||||
VERSION = "2.0.0-w8"
|
||||
|
||||
# Gemeinsame YAML-Instanz (preserve_quotes hält Kommentare/Quotes in config.yaml).
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services import agent_aktivitaet
|
||||
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
@@ -39,3 +40,13 @@ def set_brain(body: SetBrainReq) -> dict:
|
||||
def set_brain_model(body: BrainReq) -> dict:
|
||||
ok = update_brain_model(body.model)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
@router.get("/agent/aktivitaet")
|
||||
def aktivitaet(limit: int = 60) -> dict:
|
||||
"""Werkzeug-Verlauf des Agenten (v3-Umbau P6).
|
||||
|
||||
Gelesen aus Hermes' eigenem Log — MC2 patcht dort nichts, es schaut nur zu. Was
|
||||
NICHT drin steht (Denkstrom, Werkzeug-Argumente), verspricht die Ansicht auch nicht.
|
||||
"""
|
||||
return agent_aktivitaet.uebersicht(max(1, min(limit, 300)))
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Auftragsbuch-Endpoints (Vorschlags-Inbox) — dünner REST-Layer über services/auftragsbuch.py.
|
||||
LAN-only wie alle MC2-Endpoints; das Gate ist der Klick des Commanders."""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services import auftragsbuch
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class BranchIn(BaseModel):
|
||||
branch: str
|
||||
repo: str = "mc2" # "mc2" (Box-Stack) oder "lucy" (Desktop-App, Annahme baut am PC)
|
||||
grund: str = "" # nur beim Ablehnen: optionaler Grund → Lern-Gedächtnis des Kreislaufs
|
||||
|
||||
|
||||
class KandidatIn(BaseModel):
|
||||
file: str
|
||||
|
||||
|
||||
@router.get("/auftragsbuch")
|
||||
def list_proposals() -> dict:
|
||||
return auftragsbuch.list_proposals()
|
||||
|
||||
|
||||
@router.get("/auftragsbuch/diff")
|
||||
def diff(branch: str, repo: str = "mc2") -> dict:
|
||||
res = auftragsbuch.diff_of(branch, repo)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Diff nicht verfügbar."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/auftragsbuch/annehmen")
|
||||
def accept(body: BranchIn) -> dict:
|
||||
res = auftragsbuch.accept(body.branch, body.repo)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Annehmen fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/auftragsbuch/ablehnen")
|
||||
def reject(body: BranchIn) -> dict:
|
||||
res = auftragsbuch.reject(body.branch, body.repo, body.grund)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Ablehnen fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/auftragsbuch/skill/annehmen")
|
||||
def skill_accept(body: KandidatIn) -> dict:
|
||||
res = auftragsbuch.skill_accept(body.file)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Beauftragen fehlgeschlagen."))
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/auftragsbuch/skill/ablehnen")
|
||||
def skill_reject(body: KandidatIn) -> dict:
|
||||
res = auftragsbuch.skill_reject(body.file)
|
||||
if not res.get("ok"):
|
||||
raise HTTPException(400, res.get("error", "Verwerfen fehlgeschlagen."))
|
||||
return res
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Chronik — die lesbare Timeline dessen, was die Box von allein getan und gemeldet hat.
|
||||
Quelle ist der persistente Melde-Briefkasten (services/announce.py): Health-Wächter,
|
||||
Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons, Auftragsbuch — alle
|
||||
Auto-Updates, Erinnerungen, Radar/Traum/Chef-Gutachter-Crons — alle
|
||||
autonomen Kanäle laufen dort bereits durch. Hier wird nichts Neues erhoben, nur erzählt."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets (auf den Gateway + Memory-MCP)."""
|
||||
"""Connect-Endpoint: erzeugt IDE-/Agent-Snippets für den Gateway."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
||||
@@ -7,14 +7,11 @@ router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
@router.get("/connect")
|
||||
def connect(host: str = DEFAULT_HOST, mcp_path: str | None = None) -> dict:
|
||||
kwargs = {}
|
||||
if mcp_path:
|
||||
kwargs["mcp_script_path"] = mcp_path
|
||||
return build_snippets(host=host, **kwargs)
|
||||
def connect(host: str = DEFAULT_HOST) -> dict:
|
||||
return build_snippets(host=host)
|
||||
|
||||
|
||||
@router.get("/connect/health")
|
||||
def connect_health() -> dict:
|
||||
"""Live-Status der zwei Leitungen (Gateway + Gedächtnis) für den Verbinden-Tab."""
|
||||
"""Live-Status der drei Leitungen (Gateway, Gedächtnis, Desktop-Gateway) für den Verbinden-Tab."""
|
||||
return check_health()
|
||||
|
||||
+86
-40
@@ -1,25 +1,30 @@
|
||||
"""SSE-Eventstrom (UMBAU v3 P3a) — ein Kanal sagt der Zentrale, WANN neu laden lohnt.
|
||||
"""Ereignisstrom — ein Kanal sagt der Zentrale, WANN neu laden lohnt, und schickt Metriken.
|
||||
|
||||
GET /api/events liefert Server-Sent Events. Ein Sammler prüft alle paar Sekunden
|
||||
billige Fingerabdrücke der ereignishaften Quellen und schickt NUR bei Änderung ein
|
||||
`invalidate`-Event mit den React-Query-Keys. Die Wahrheit bleibt in den bestehenden
|
||||
Endpunkten — der Strom ist ein reiner Invalidation-Bus, kein zweites Zustandsmodell.
|
||||
Zwei Endpunkte, ein Sammler:
|
||||
|
||||
Quellen: Briefkasten/Chronik (in-process-Cursor), Ideen-Queue ((id,status)-Paare),
|
||||
Auftragsbuch + Erinnerungen (Datei-mtimes), geladene Modelle (Running-Set — Idee aus
|
||||
der Werkstatt-Karte feature/sse-backend-v1). BEWUSST NICHT dabei: System-/Token-
|
||||
Metriken (ändern sich jede Sekunde — da ist Polling das richtige Werkzeug und ein
|
||||
invalidate-Event nur Lärm).
|
||||
GET /api/stream (v3-Umbau P4, 28.08.2026) — der aktuelle Kanal. Zwei Ereignisarten:
|
||||
· `invalidate` Nur bei Änderung, mit den betroffenen React-Query-Schlüsseln.
|
||||
· `metrik` Jede Sekunde ein Messpunkt (CPU/RAM/GPU/Temp/Token-Zähler).
|
||||
|
||||
Versöhnt 15.07. abends: Die angenommene Werkstatt-Version nutzte `type:` statt
|
||||
`event:` (ungültiges SSE-Framing → EventSource-Listener feuert NIE), einen globalen
|
||||
Snapshot über alle Clients und einen nicht existierenden Ideen-Endpunkt — Kern
|
||||
wieder die getestete Hand-Implementierung (E2E: Announce → invalidate binnen
|
||||
Sekunden), Modell-Quelle aus der Karte übernommen.
|
||||
GET /api/events — der alte Kanal, nur `invalidate`. Bleibt EINE Fassung lang stehen,
|
||||
weil ein Browser-Tab nach einem Deploy noch das vorige Bündel halten kann und dieses
|
||||
nur `/api/events` kennt. Danach entfernen.
|
||||
|
||||
Frontend-Gegenstück: frontend/src/lib/events.ts (EventSource, invalidiert die
|
||||
Caches, entspannt die Fallback-Poller ×5; reißt der Strom, reconnectet EventSource
|
||||
selbst und bis dahin pollt die UI wie bisher).
|
||||
WARUM METRIKEN JETZT MITKOMMEN: Bis P4 pollte das Frontend `/api/system/status` und
|
||||
`/api/system/token-stats` im 3-Sekunden-Takt — zwei Dauer-Anfragen, unabhängig davon, ob
|
||||
sich etwas geändert hat, plus sechs weitere langsamere Poller auf der Startseite. Der
|
||||
Messpunkt kostet hier 0,2 ms (gemessen); `system_status()` würde 100 ms kosten, weil
|
||||
`psutil.cpu_percent(interval=0.1)` wartet. Deshalb der eigene, leichte `metrik_punkt()`.
|
||||
|
||||
WAS BEWUSST NICHT DRIN IST: Ein `agent`-Thema für Lucys Denkschritte. MC2 kann Hermes'
|
||||
interne Schritte nicht sehen, ohne dessen Quellcode zu patchen — und das ist per AGENTS.md
|
||||
verboten. Eine leere Leitung zu bauen, wäre eine Zusage, die keiner einlöst.
|
||||
|
||||
Die Wahrheit bleibt in den bestehenden Endpunkten: `invalidate` ist ein reiner
|
||||
Anstoß-Bus, kein zweites Zustandsmodell. `metrik` ist die einzige Ausnahme — es ist der
|
||||
Wert selbst, weil ein Anstoß für eine Zahl, die sich jede Sekunde ändert, nur Lärm wäre.
|
||||
|
||||
Frontend-Gegenstück: frontend/src/lib/events.ts
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -35,7 +40,8 @@ log = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
TICK_S = 3.0 # Prüf-Takt des Sammlers (nur Fingerabdrücke, kein Neuberechnen)
|
||||
METRIK_S = 1.0 # Takt der Messpunkte
|
||||
ABDRUCK_S = 3.0 # Takt der Änderungs-Prüfung (nur Fingerabdrücke, kein Neuberechnen)
|
||||
KEEPALIVE_S = 20.0 # Kommentar-Ping, damit Proxies/Browser die Verbindung halten
|
||||
|
||||
|
||||
@@ -66,35 +72,75 @@ def _fingerprints() -> dict[str, object]:
|
||||
fp["models"] = json.dumps(sorted(str(m) for m in llamaswap.get_running_models()))
|
||||
except Exception:
|
||||
pass
|
||||
# Auftragsbuch (Annahme-Status + Karten-Meldungen) & Erinnerungen: Datei-mtimes
|
||||
fp["auftragsbuch"] = (_mtime(MODELS_DIR / "mc2-auftragsbuch.json"),
|
||||
_mtime(MODELS_DIR / "mc2-announce-branches.json"))
|
||||
try: # Jobs (Downloads, Wartung): Zustand + Fortschritt — spart den 3-s-Poller der Schublade
|
||||
from services import jobengine
|
||||
fp["jobs"] = json.dumps(
|
||||
[(j.get("id"), j.get("state"), j.get("progress")) for j in jobengine.public_jobs()]
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# Erinnerungen: Datei-mtime (das Auftragsbuch ist seit 04.09.2026 ausgebaut)
|
||||
fp["reminders"] = _mtime(MODELS_DIR / "mc2-reminders.json")
|
||||
return fp
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def events(request: Request) -> StreamingResponse:
|
||||
async def strom():
|
||||
# Basislinie JE VERBINDUNG (der Client hat beim Verbinden frisch geladen) —
|
||||
# ein globaler Snapshot würde bei mehreren Clients Events verschlucken.
|
||||
alt = _fingerprints()
|
||||
yield ": verbunden\n\n"
|
||||
seit_ping = 0.0
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
return
|
||||
await asyncio.sleep(TICK_S)
|
||||
seit_ping += TICK_S
|
||||
async def _strom(request: Request, mit_metrik: bool):
|
||||
"""Gemeinsamer Kern beider Endpunkte.
|
||||
|
||||
Die Basislinie entsteht JE VERBINDUNG (der Client hat beim Verbinden frisch geladen) —
|
||||
ein globaler Snapshot würde bei mehreren Clients Events verschlucken.
|
||||
"""
|
||||
alt = _fingerprints()
|
||||
yield ": verbunden\n\n"
|
||||
|
||||
seit_abdruck = 0.0
|
||||
seit_ping = 0.0
|
||||
takt = METRIK_S if mit_metrik else ABDRUCK_S
|
||||
|
||||
while True:
|
||||
if await request.is_disconnected():
|
||||
return
|
||||
await asyncio.sleep(takt)
|
||||
seit_abdruck += takt
|
||||
seit_ping += takt
|
||||
|
||||
if mit_metrik:
|
||||
try:
|
||||
from services.system import metrik_punkt
|
||||
yield f"event: metrik\ndata: {json.dumps(metrik_punkt())}\n\n"
|
||||
seit_ping = 0.0
|
||||
except Exception:
|
||||
# Ein kaputter Messpunkt darf den Strom nicht reißen — die Ansicht fällt
|
||||
# dann auf ihre Poller zurück, das ist besser als eine tote Leitung.
|
||||
log.warning("Messpunkt fehlgeschlagen", exc_info=True)
|
||||
|
||||
if seit_abdruck >= ABDRUCK_S:
|
||||
seit_abdruck = 0.0
|
||||
neu = _fingerprints()
|
||||
keys = [k for k, v in neu.items() if k in alt and v != alt[k]]
|
||||
alt.update(neu)
|
||||
if keys:
|
||||
yield f"event: invalidate\ndata: {json.dumps({'keys': keys})}\n\n"
|
||||
seit_ping = 0.0
|
||||
elif seit_ping >= KEEPALIVE_S:
|
||||
yield ": ping\n\n"
|
||||
seit_ping = 0.0
|
||||
|
||||
return StreamingResponse(strom(), media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
if seit_ping >= KEEPALIVE_S:
|
||||
yield ": ping\n\n"
|
||||
seit_ping = 0.0
|
||||
|
||||
|
||||
_KOPF = {"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
|
||||
|
||||
|
||||
@router.get("/stream")
|
||||
async def stream(request: Request) -> StreamingResponse:
|
||||
"""Der aktuelle Kanal: Anstöße UND Messpunkte."""
|
||||
return StreamingResponse(_strom(request, mit_metrik=True),
|
||||
media_type="text/event-stream", headers=_KOPF)
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def events(request: Request) -> StreamingResponse:
|
||||
"""Alt-Kanal ohne Messpunkte. Nur für Browser-Tabs, die noch ein Bündel von vor
|
||||
dem 28.08.2026 halten. Mit der übernächsten Fassung entfernen."""
|
||||
return StreamingResponse(_strom(request, mit_metrik=False),
|
||||
media_type="text/event-stream", headers=_KOPF)
|
||||
|
||||
@@ -22,6 +22,13 @@ _LANG_DIRECTIVE = os.environ.get(
|
||||
"Quellcode, Bezeichner und Shell-Befehle bleiben unverändert.")
|
||||
|
||||
|
||||
# Deckel für die Bild-Weiche. Ohne ihn wächst die Wartezeit linear mit der Bildzahl:
|
||||
# je Bild bis zu 2 Versuche à _BILD_TIMEOUT_S, und der Client hängt so lange am offenen
|
||||
# Request. Über _BILD_MAX Bilder wird gar nicht erst beschrieben — der Aufrufer fällt
|
||||
# dann auf die normale Vision-Umleitung zurück (ehrlich langsam statt scheinbar hängend).
|
||||
_BILD_TIMEOUT_S = float(os.environ.get("MC_CODER_IMAGE_TIMEOUT_S", "240"))
|
||||
_BILD_MAX = int(os.environ.get("MC_CODER_IMAGE_MAX", "4"))
|
||||
|
||||
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
||||
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
||||
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
||||
@@ -85,6 +92,10 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
||||
for i, part in enumerate(m["content"]):
|
||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||
fundstellen.append((m, i, part))
|
||||
if len(fundstellen) > _BILD_MAX:
|
||||
log.warning("Bild-Weiche v2: %s Bilder (Deckel %s) — Request geht an das Vision-Modell "
|
||||
"statt einzeln beschrieben zu werden", len(fundstellen), _BILD_MAX)
|
||||
return False
|
||||
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
||||
frage = {
|
||||
"model": vision_alias,
|
||||
@@ -99,7 +110,7 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
||||
text = ""
|
||||
for versuch in (1, 2):
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=240.0) as c:
|
||||
async with httpx.AsyncClient(timeout=_BILD_TIMEOUT_S) as c:
|
||||
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||
if r.status_code == 200:
|
||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
||||
@@ -138,7 +149,15 @@ def _inject_language(body: dict, alias: str) -> None:
|
||||
async def models(request: Request):
|
||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
||||
r = await client.get(f"{LLAMA_SWAP_URL}/v1/models", timeout=10.0)
|
||||
data = r.json()
|
||||
try:
|
||||
data = r.json()
|
||||
except ValueError:
|
||||
# Engine antwortet, aber nicht mit JSON (Startphase/Fehlerseite) — ehrlicher 502
|
||||
# statt eines 500ers aus dem Parser.
|
||||
log.warning("/v1/models: Engine-Antwort ist kein JSON (HTTP %s): %.200s", r.status_code, r.text)
|
||||
return JSONResponse(
|
||||
{"error": {"message": "Engine lieferte keine gültige Modell-Liste.",
|
||||
"type": "upstream_error"}}, status_code=502)
|
||||
# Aufgeräumt (07/2026): Die Router-Lanes (coding/chat) werden NICHT mehr als „Modell"
|
||||
# angeboten — sie verwirrten (coding vs. coder) und seit dem Zed-Aus wählt sie niemand
|
||||
# mehr. Das Routing bleibt erhalten (siehe _proxy), sie stehen nur nicht mehr in der Liste.
|
||||
@@ -187,7 +206,20 @@ async def models(request: Request):
|
||||
|
||||
|
||||
async def _proxy(path: str, request: Request):
|
||||
body = await request.json()
|
||||
# Ungültige Payloads gehören dem Client, nicht dem Server: ohne diese Prüfung wirft
|
||||
# request.json() bzw. body.get(...) durch und der Aufrufer bekommt einen 500er
|
||||
# (gemessen 27.08.2026: Rohtext, leerer Body, JSON-Liste, JSON-String und null — 5/5 mal 500).
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": {"message": "Ungültiger Request-Body: JSON erwartet.",
|
||||
"type": "invalid_request_error"}}, status_code=400)
|
||||
if not isinstance(body, dict):
|
||||
return JSONResponse(
|
||||
{"error": {"message": "Ungültiger Request-Body: JSON-Objekt erwartet, "
|
||||
f"'{type(body).__name__}' bekommen.",
|
||||
"type": "invalid_request_error"}}, status_code=400)
|
||||
_apply_no_think_marker(body) # Lucy-Voice-Schnellspur: Marker strippen + Thinking aus
|
||||
requested = str(body.get("model") or "auto")
|
||||
if requested.lower() in ("auto", "chat", "coding"):
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
"""Wartungs-Endpoints: Update-Badge, OS-/Engine-Update, Reboot, Restart, Logs."""
|
||||
"""Wartungs-Endpoints: Update-Badge, OS-/Engine-Update, Reboot, Restart, Logs.
|
||||
|
||||
from fastapi import APIRouter, Header, HTTPException
|
||||
v3-Umbau P1 (28.08.2026): Das Sudo-Passwort ist hier ersatzlos entfallen. Auf der Box
|
||||
gemessen — `sudo -n true` läuft durch, weil `/etc/sudoers` den Dienst-Nutzer mit
|
||||
`NOPASSWD: ALL` führt. Das Passwort wurde also nie gebraucht, lag aber im
|
||||
`localStorage` des Browsers und reiste bei jedem mutierenden Request mit. Sollte die
|
||||
sudoers-Zeile je fallen, meldet `services.maintenance` sauber `password_required`
|
||||
statt still zu scheitern — das ist dann ein Konfigurations-Signal und nichts, was man
|
||||
mit einem im Browser geparkten Geheimnis übertüncht.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services import maintenance
|
||||
from services import geheimnisse, maintenance
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
class SudoReq(BaseModel):
|
||||
sudo_password: str | None = None
|
||||
|
||||
|
||||
class RestartReq(BaseModel):
|
||||
service: str
|
||||
sudo_password: str | None = None
|
||||
|
||||
|
||||
class GeheimnisReq(BaseModel):
|
||||
schluessel: str
|
||||
wert: str | None = None
|
||||
|
||||
|
||||
@router.get("/maintenance/updates")
|
||||
@@ -28,32 +37,32 @@ def update_details(kind: str) -> dict:
|
||||
return maintenance.update_details(kind)
|
||||
|
||||
@router.post("/maintenance/check-updates")
|
||||
def check_updates(body: SudoReq) -> dict:
|
||||
res = maintenance.check_updates_job(body.sudo_password)
|
||||
def check_updates() -> dict:
|
||||
res = maintenance.check_updates_job()
|
||||
if isinstance(res, dict) and not res.get("ok", True):
|
||||
return res
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/maintenance/os-update")
|
||||
def os_update(body: SudoReq) -> dict:
|
||||
res = maintenance.os_update_job(body.sudo_password)
|
||||
def os_update() -> dict:
|
||||
res = maintenance.os_update_job()
|
||||
if isinstance(res, dict) and not res.get("ok", True):
|
||||
return res
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/maintenance/engine-update")
|
||||
def engine_update(body: SudoReq) -> dict:
|
||||
res = maintenance.engine_update_job(body.sudo_password)
|
||||
def engine_update() -> dict:
|
||||
res = maintenance.engine_update_job()
|
||||
if not res:
|
||||
raise HTTPException(400, "Kein Engine-Update-Befehl gesetzt (MC_ENGINE_UPDATE_CMD).")
|
||||
return res
|
||||
|
||||
|
||||
@router.post("/maintenance/swap-update")
|
||||
def swap_update(body: SudoReq) -> dict:
|
||||
res = maintenance.swap_update_job(body.sudo_password)
|
||||
def swap_update() -> dict:
|
||||
res = maintenance.swap_update_job()
|
||||
if not res:
|
||||
raise HTTPException(400, "Kein Router-Update-Befehl gesetzt (MC_SWAP_UPDATE_CMD).")
|
||||
return res
|
||||
@@ -65,20 +74,34 @@ def hermes_update() -> dict:
|
||||
|
||||
|
||||
@router.post("/maintenance/update-all")
|
||||
def update_all(body: SudoReq) -> dict:
|
||||
return maintenance.update_all_job(body.sudo_password)
|
||||
def update_all() -> dict:
|
||||
return maintenance.update_all_job()
|
||||
|
||||
|
||||
@router.post("/maintenance/reboot")
|
||||
def reboot(body: SudoReq) -> dict:
|
||||
return maintenance.reboot(body.sudo_password)
|
||||
def reboot() -> dict:
|
||||
return maintenance.reboot()
|
||||
|
||||
|
||||
@router.post("/maintenance/restart")
|
||||
def restart(body: RestartReq) -> dict:
|
||||
return maintenance.restart_service(body.service, body.sudo_password)
|
||||
return maintenance.restart_service(body.service)
|
||||
|
||||
|
||||
@router.get("/maintenance/logs")
|
||||
def logs(service: str, lines: int = 200, x_sudo_password: str | None = Header(None)) -> dict:
|
||||
return maintenance.logs(service, lines, x_sudo_password)
|
||||
def logs(service: str, lines: int = 200) -> dict:
|
||||
return maintenance.logs(service, lines)
|
||||
|
||||
|
||||
@router.get("/maintenance/geheimnisse")
|
||||
def geheimnisse_status() -> dict:
|
||||
"""Nur der Zustand — ob ein Token gesetzt ist, niemals sein Wert."""
|
||||
return geheimnisse.status()
|
||||
|
||||
|
||||
@router.post("/maintenance/geheimnisse")
|
||||
def geheimnisse_setzen(body: GeheimnisReq) -> dict:
|
||||
"""Setzt (oder löscht bei leerem Wert) ein Geheimnis in der Box-Ablage."""
|
||||
if not geheimnisse.setzen(body.schluessel, body.wert):
|
||||
raise HTTPException(400, f"Geheimnis '{body.schluessel}' konnte nicht gespeichert werden.")
|
||||
return {"ok": True, **geheimnisse.status()}
|
||||
|
||||
@@ -4,7 +4,7 @@ import psutil
|
||||
from config import HF_DOWNLOAD_ENV, MODELS_DIR
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from services import budget, discover, hf, jobengine, llamaswap
|
||||
from services import budget, discover, geheimnisse, hf, jobengine, llamaswap
|
||||
from services.fit import evaluate_fit, max_ctx_for
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
@@ -77,7 +77,9 @@ class InstallReq(BaseModel):
|
||||
quant: str = "Q4_K_M"
|
||||
ctx: int | None = None
|
||||
jinja: bool = False
|
||||
hf_token: str | None = None
|
||||
# Kein hf_token mehr im Request (v3-Umbau P1): der Token lag frueher im localStorage
|
||||
# des Browsers und reiste hier mit. Jetzt liegt er auf der Box (services.geheimnisse)
|
||||
# und wird unten von dort gelesen — die Oberflaeche sieht ihn nie wieder.
|
||||
|
||||
|
||||
@router.get("/hf/search")
|
||||
@@ -144,8 +146,8 @@ def install(req: InstallReq) -> dict:
|
||||
args.append(info["mmproj"])
|
||||
args += ["--local-dir", str(target)]
|
||||
env = dict(HF_DOWNLOAD_ENV)
|
||||
if req.hf_token:
|
||||
env["HF_TOKEN"] = req.hf_token
|
||||
if token := geheimnisse.hf_token():
|
||||
env["HF_TOKEN"] = token
|
||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
|
||||
on_done=_apply_role if role else None)
|
||||
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
||||
|
||||
@@ -7,6 +7,20 @@ from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
|
||||
def _hermes_name(ordner: str) -> str:
|
||||
"""Repo-Ordnername -> Hermes-Skillname.
|
||||
|
||||
deploy/deploy.sh kopiert `deploy/skills/<name>` nach `~/.hermes/skills/<name mit _>`
|
||||
(`tr '-' '_'`), weil Hermes keine Bindestriche in Skillnamen mag. Ohne dieselbe
|
||||
Umschreibung hier bekam Hermes den Ordnernamen MIT Bindestrich und fand den Skill
|
||||
nicht - betroffen waren 7 der 12 Skills (betrieb-playbook, konzept-fliessband,
|
||||
llm-wiki, morning-report, pc-pfad-cache, projekt-start, trend-radar): der Startknopf
|
||||
meldete Erfolg, ausgefuehrt wurde nichts. deploy.sh bleibt die Quelle der Wahrheit.
|
||||
"""
|
||||
return ordner.replace("-", "_")
|
||||
|
||||
|
||||
class RunSkillRequest(BaseModel):
|
||||
skill_name: str
|
||||
|
||||
@@ -57,8 +71,11 @@ def list_skills():
|
||||
|
||||
skills.append({
|
||||
"name": entry.name,
|
||||
# Der Name, unter dem Hermes den Skill kennt - auch fuer den Laufend-
|
||||
# Vergleich, weil im Prozess-cmdline genau dieser Name steht.
|
||||
"hermes_name": _hermes_name(entry.name),
|
||||
"description": description or "Keine Beschreibung verfügbar.",
|
||||
"running": entry.name in running_skills
|
||||
"running": _hermes_name(entry.name) in running_skills
|
||||
})
|
||||
return {"skills": skills}
|
||||
|
||||
@@ -70,13 +87,15 @@ def run_skill(req: RunSkillRequest):
|
||||
# Fallback falls lokal (auf Windows) entwickelt wird
|
||||
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
||||
|
||||
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat
|
||||
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
|
||||
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat - mit dem Namen, den Hermes
|
||||
# nach dem Deploy-Sync wirklich kennt (Bindestriche -> Unterstriche, s. _hermes_name).
|
||||
skill = _hermes_name(req.skill_name)
|
||||
cmd = [hermes_bin, "-z", f"Führe den '{skill}' Skill aus", "chat"]
|
||||
try:
|
||||
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{req.skill_name.replace('/', '_')}.log")
|
||||
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{skill.replace('/', '_')}.log")
|
||||
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||
with open(log_path, "a") as f:
|
||||
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(f"\n--- Starting Skill: {skill} ---\n")
|
||||
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
||||
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
||||
except Exception as e:
|
||||
|
||||
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
|
||||
|
||||
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
||||
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
||||
geteiltem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||
eigenem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
||||
|
||||
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||
@@ -17,7 +17,7 @@ import sys as _sys
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL
|
||||
from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, LUCY_STIMME_URL, VOICE_SERVICE_URL
|
||||
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import Response, StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
|
||||
@router.get("/voice/trace")
|
||||
def voice_trace(limit: int = 20) -> dict:
|
||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
||||
Generierung, Gedächtnis als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||
Generierung). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||
return {"turns": get_trace(limit)}
|
||||
|
||||
@@ -289,8 +289,8 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
headers["X-Hermes-Session-Key"] = body.session_key
|
||||
|
||||
async def gen():
|
||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Gedächtnis
|
||||
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung zu EINEM
|
||||
# Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||
first = True
|
||||
first_content = True
|
||||
@@ -325,7 +325,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Gedächtnis-Extraktion.
|
||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py).
|
||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
try:
|
||||
@@ -355,3 +355,71 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
||||
_commit() # Turn immer verbuchen (auch bei Fehler/Abbruch)
|
||||
|
||||
return StreamingResponse(gen(), media_type="text/event-stream")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Lucys Stimme ins LAN reichen (Raphael-Umbau 04.09.2026). lucy-stimme.service (:8021, pocket-tts
|
||||
# german_24l) bindet nur Loopback; die Desktop-Lucy am PC spricht seit dem Umbau nicht mehr mit
|
||||
# einem eigenen pocket_server, sondern mit DIESEM — dieselbe Stimme wie die Telegram-Sprachnachrichten.
|
||||
# Dünner Proxy, API 1:1 (pocket_server: /health, /tts -> WAV, /tts/stream -> PCM16 + X-Sample-Rate).
|
||||
# LAN-only wie alle MC2-Endpoints.
|
||||
|
||||
class LucyTtsIn(BaseModel):
|
||||
text: str
|
||||
emo: str | None = None # Stimmungs-Profil (pocket_server EMO_PROFILES); Raphael-Lucy setzt keins
|
||||
|
||||
|
||||
@router.get("/lucy/stimme/health")
|
||||
def lucy_stimme_health() -> dict:
|
||||
"""Bereitschaft von Lucys Stimme (pocket_server /health: status ok|loading)."""
|
||||
try:
|
||||
r = httpx.get(f"{LUCY_STIMME_URL}/health", timeout=httpx.Timeout(5.0))
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except Exception as exc:
|
||||
raise HTTPException(502, f"Lucys Stimme (:8021) nicht erreichbar: {exc}")
|
||||
|
||||
|
||||
@router.post("/lucy/stimme/tts")
|
||||
async def lucy_stimme_tts(body: LucyTtsIn) -> Response:
|
||||
"""Text -> WAV (ganzer Text). Warm-up der Desktop-Lucy + Jobs, die eine Datei brauchen."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(600.0, connect=5.0)) as client:
|
||||
with Timer("lucy_tts"):
|
||||
r = await client.post(f"{LUCY_STIMME_URL}/tts", json=body.model_dump(exclude_none=True))
|
||||
r.raise_for_status()
|
||||
return Response(content=r.content, media_type=r.headers.get("content-type", "audio/wav"),
|
||||
headers={k: v for k, v in r.headers.items() if k.lower().startswith("x-")})
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(exc.response.status_code, f"Lucys Stimme: {exc.response.text[:200]}")
|
||||
except httpx.HTTPError as exc:
|
||||
raise HTTPException(502, f"Lucys Stimme nicht erreichbar: {exc}")
|
||||
|
||||
|
||||
@router.post("/lucy/stimme/tts/stream")
|
||||
async def lucy_stimme_tts_stream(body: LucyTtsIn) -> StreamingResponse:
|
||||
"""Text -> rohes PCM16-mono, satzweise gestreamt (Samplerate im Header X-Sample-Rate).
|
||||
Der Live-Pfad der Desktop-Lucy: erstes Audio nach dem ersten Satz. Der Upstream-Stream bleibt
|
||||
offen, solange der Client liest — bricht der Client ab (Barge-in), schließt httpx den Upstream."""
|
||||
client = httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0))
|
||||
try:
|
||||
req = client.build_request("POST", f"{LUCY_STIMME_URL}/tts/stream", json=body.model_dump(exclude_none=True))
|
||||
upstream = await client.send(req, stream=True)
|
||||
except httpx.HTTPError as exc:
|
||||
await client.aclose()
|
||||
raise HTTPException(502, f"Lucys Stimme nicht erreichbar: {exc}")
|
||||
if upstream.status_code != 200:
|
||||
detail = (await upstream.aread()).decode("utf-8", "replace")[:200]
|
||||
await upstream.aclose(); await client.aclose()
|
||||
raise HTTPException(upstream.status_code, f"Lucys Stimme: {detail}")
|
||||
|
||||
async def gen():
|
||||
try:
|
||||
async for chunk in upstream.aiter_raw():
|
||||
yield chunk
|
||||
finally:
|
||||
await upstream.aclose()
|
||||
await client.aclose()
|
||||
|
||||
return StreamingResponse(gen(), media_type="application/octet-stream",
|
||||
headers={"X-Sample-Rate": upstream.headers.get("x-sample-rate", "24000")})
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Werkzeug-Verlauf des Agenten (v3-Umbau P6).
|
||||
|
||||
WARUM ES DAS GIBT: Lucys Arbeit war eine Blackbox mit Statuswort. Der Blueprint sah
|
||||
dafür eine „Live Agent Matrix" mit Denkstrom vor (§4.4) — die ist so nicht baubar: Die
|
||||
Denkschritte entstehen im Hermes-Prozess, und `AGENTS.md` verbietet es, dessen Quellcode
|
||||
zu patchen.
|
||||
|
||||
Beim Nachsehen zeigte sich aber, dass die HÄLFTE davon längst offen daliegt: Hermes
|
||||
protokolliert jeden Werkzeug-Ruf nach `~/.hermes/logs/agent.log`. Eine Log-Datei zu lesen
|
||||
ist kein Patchen. Was dadurch sichtbar wird — welches Werkzeug, wie lange, mit welchem
|
||||
Ergebnis, in welchem Lauf — ist für die Frage „was tut sie gerade und wo hängt es" oft
|
||||
nützlicher als der Fließtext ihrer Gedanken.
|
||||
|
||||
WAS NICHT GEHT (und hier auch nicht so tut): der Denkstrom selbst und die Argumente eines
|
||||
Werkzeug-Rufs. Beides steht nicht im Log. Die Ansicht verspricht deshalb nur, was sie
|
||||
halten kann.
|
||||
|
||||
ES HAT SICH SOFORT GELOHNT: Beim ersten Lesen fiel auf, dass `web_extract` seit
|
||||
mindestens dem 25.08. JEDEN Morgen um 07:00 scheitert (der Suchanbieter kann keine
|
||||
Seiten abrufen). Vier Tage lang, ohne dass es irgendwo aufgefallen wäre.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Die Box läuft in Europe/Berlin (AGENTS.md). Hermes' Log trägt Ortszeit ohne Offset.
|
||||
LOCAL_TZ = ZoneInfo(os.environ.get("MC_LOCAL_TZ", "Europe/Berlin"))
|
||||
|
||||
LOG_PFAD = Path(os.path.expanduser(
|
||||
os.environ.get("MC_HERMES_AGENT_LOG", "~/.hermes/logs/agent.log")))
|
||||
|
||||
# Nur das Ende der Datei lesen. Sie wächst auf mehrere MB und wird rotiert; für einen
|
||||
# Verlauf der letzten Stunden reicht der Schwanz — und er kostet nichts.
|
||||
LESE_BYTES = 512 * 1024
|
||||
|
||||
# Die drei Formen, in denen Hermes einen Werkzeug-Ruf notiert (am Log gemessen, nicht
|
||||
# geraten). Die Lauf-Kennung in eckigen Klammern fehlt bei Nicht-Cron-Läufen.
|
||||
#
|
||||
# INFO [cron_…] agent.tool_executor: tool terminal completed (1.40s, 53 chars)
|
||||
# INFO agent.tool_executor: tool web_search completed (2.61s, 2944 chars)
|
||||
# WARNING [cron_…] agent.tool_executor: Tool web_extract returned error (0.17s): {…}
|
||||
# INFO agent.tool_executor: tool web_extract failed (0.17s): {…}
|
||||
_ZEIT = r"(?P<zeit>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+"
|
||||
_LAUF = r"(?:\[(?P<lauf>[^\]]+)\] )?"
|
||||
|
||||
_FERTIG = re.compile(
|
||||
_ZEIT + r" \w+ " + _LAUF + r"agent\.tool_executor: [Tt]ool (?P<werkzeug>\S+) completed "
|
||||
r"\((?P<dauer>[\d.]+)s, (?P<zeichen>\d+) chars\)")
|
||||
|
||||
_FEHLER = re.compile(
|
||||
_ZEIT + r" \w+ " + _LAUF + r"agent\.tool_executor: [Tt]ool (?P<werkzeug>\S+) "
|
||||
r"(?:failed|returned error) \((?P<dauer>[\d.]+)s\)(?::\s*(?P<detail>.*))?")
|
||||
|
||||
|
||||
def _schwanz(pfad: Path, n: int) -> list[str]:
|
||||
"""Die letzten n Bytes als Zeilen. Die erste Zeile kann angeschnitten sein und
|
||||
wird verworfen — ein halber Zeitstempel passt auf kein Muster, aber sicher ist besser."""
|
||||
try:
|
||||
groesse = pfad.stat().st_size
|
||||
with pfad.open("rb") as f:
|
||||
f.seek(max(0, groesse - n))
|
||||
roh = f.read()
|
||||
zeilen = roh.decode("utf-8", errors="replace").splitlines()
|
||||
return zeilen[1:] if groesse > n and zeilen else zeilen
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def _kurz(detail: str | None) -> str | None:
|
||||
"""Fehlertext des Werkzeugs auf einen lesbaren Satz eindampfen. Hermes legt dort ein
|
||||
JSON ab; interessant ist daran nur das `error`-Feld."""
|
||||
if not detail:
|
||||
return None
|
||||
treffer = re.search(r'"error"\s*:\s*"([^"]{1,300})"', detail)
|
||||
text = treffer.group(1) if treffer else detail.strip()
|
||||
return (text[:297] + "…") if len(text) > 300 else text
|
||||
|
||||
|
||||
def rufe(limit: int = 60) -> list[dict]:
|
||||
"""Die jüngsten Werkzeug-Rufe, ältester zuerst."""
|
||||
ergebnis: list[dict] = []
|
||||
for zeile in _schwanz(LOG_PFAD, LESE_BYTES):
|
||||
m = _FERTIG.match(zeile)
|
||||
if m:
|
||||
ergebnis.append({
|
||||
"zeit": _iso(m.group("zeit")),
|
||||
"lauf": m.group("lauf"),
|
||||
"werkzeug": m.group("werkzeug"),
|
||||
"dauer_s": float(m.group("dauer")),
|
||||
"zeichen": int(m.group("zeichen")),
|
||||
"ok": True,
|
||||
"fehler": None,
|
||||
})
|
||||
continue
|
||||
m = _FEHLER.match(zeile)
|
||||
if m:
|
||||
ergebnis.append({
|
||||
"zeit": _iso(m.group("zeit")),
|
||||
"lauf": m.group("lauf"),
|
||||
"werkzeug": m.group("werkzeug"),
|
||||
"dauer_s": float(m.group("dauer")),
|
||||
"zeichen": None,
|
||||
"ok": False,
|
||||
"fehler": _kurz(m.group("detail")),
|
||||
})
|
||||
return ergebnis[-limit:]
|
||||
|
||||
|
||||
def _iso(s: str) -> str:
|
||||
"""`2026-08-28 07:03:18` → ISO MIT Zeitzone.
|
||||
|
||||
Hermes schreibt seine Log-Zeitstempel in Ortszeit ohne Offset. Die naiv zu lassen
|
||||
wäre bequem (der Klient zeigt sie nur an) — aber genau daran hängt eine
|
||||
Projektregel: Naive Zeiten werden über MC_LOCAL_TZ aufgelöst, nie geraten
|
||||
(AGENTS.md; ruff DTZ007 erzwingt es). Sobald jemand später damit rechnet — Dauer
|
||||
über Mitternacht, Vergleich mit einem Cron-Plan — wäre eine offsetlose Zeit eine
|
||||
Falle, die erst zur Zeitumstellung zuschnappt."""
|
||||
try:
|
||||
return datetime.strptime(s, "%Y-%m-%d %H:%M:%S").replace(tzinfo=LOCAL_TZ).isoformat()
|
||||
except ValueError:
|
||||
return s
|
||||
|
||||
|
||||
def uebersicht(limit: int = 60) -> dict:
|
||||
"""Was die Agent-Ansicht braucht: die Rufe selbst, je Werkzeug eine Bilanz und die
|
||||
wiederkehrenden Fehler zusammengefasst.
|
||||
|
||||
Die Bilanz ist der eigentliche Nutzen: Ein Werkzeug, das IMMER scheitert, verschwindet
|
||||
in einer Zeitleiste — in einer Zeile „web_extract · 4 Rufe · 4 Fehler" nicht."""
|
||||
liste = rufe(limit)
|
||||
if not LOG_PFAD.exists():
|
||||
return {"verfuegbar": False, "pfad": str(LOG_PFAD), "rufe": [],
|
||||
"werkzeuge": [], "laeufe": []}
|
||||
|
||||
bilanz: dict[str, dict] = {}
|
||||
for r in liste:
|
||||
b = bilanz.setdefault(r["werkzeug"], {
|
||||
"werkzeug": r["werkzeug"], "rufe": 0, "fehler": 0,
|
||||
"dauer_summe": 0.0, "letzter_fehler": None,
|
||||
})
|
||||
b["rufe"] += 1
|
||||
b["dauer_summe"] += r["dauer_s"]
|
||||
if not r["ok"]:
|
||||
b["fehler"] += 1
|
||||
b["letzter_fehler"] = r["fehler"]
|
||||
|
||||
werkzeuge = sorted(
|
||||
({**b, "dauer_schnitt_s": round(b["dauer_summe"] / max(b["rufe"], 1), 2)}
|
||||
for b in bilanz.values()),
|
||||
key=lambda b: (-b["fehler"], -b["rufe"]),
|
||||
)
|
||||
|
||||
# Läufe in der Reihenfolge ihres ersten Auftretens (nicht sortiert nach Kennung —
|
||||
# die trägt zwar ein Datum, aber darauf sollte sich niemand verlassen).
|
||||
laeufe: list[dict] = []
|
||||
gesehen: dict[str, dict] = {}
|
||||
for r in liste:
|
||||
schluessel = r["lauf"] or "(interaktiv)"
|
||||
if schluessel not in gesehen:
|
||||
gesehen[schluessel] = {"lauf": schluessel, "von": r["zeit"], "bis": r["zeit"],
|
||||
"rufe": 0, "fehler": 0}
|
||||
laeufe.append(gesehen[schluessel])
|
||||
e = gesehen[schluessel]
|
||||
e["bis"] = r["zeit"]
|
||||
e["rufe"] += 1
|
||||
if not r["ok"]:
|
||||
e["fehler"] += 1
|
||||
|
||||
return {"verfuegbar": True, "pfad": str(LOG_PFAD), "rufe": liste,
|
||||
"werkzeuge": werkzeuge, "laeufe": laeufe}
|
||||
@@ -1,525 +0,0 @@
|
||||
"""
|
||||
Auftragsbuch — die Vorschlags-Inbox der Box (das Mensch-Gate als Klick statt Git-Handarbeit).
|
||||
|
||||
Quellen der Karten:
|
||||
• Vorschlags-Branches auf Gitea (wartung/*, orchestrator/*, doku/*) — die Werkstatt und der
|
||||
Orchestrator arbeiten propose-only und lassen ihre Ergebnisse dort liegen.
|
||||
Seit S3 (lucy-pipeline) aus ZWEI Repos: mission-control-v2 (Box-Stack) UND lucy (Desktop-App).
|
||||
• Skill-Kandidaten aus dem Wissens-Vault (~/wissens-vault/skill-kandidaten/) — Vorschläge des
|
||||
nächtlichen Traum-Crons, Gate war bisher „Commander sagt mach".
|
||||
|
||||
Annehmen (Branch) startet den Repo-eigenen Runner als EIGENE systemd-Unit (detached):
|
||||
• mc2: deploy/auftrag-annehmen.sh — Merge im Worktree → Push main → Deploy → Health → Revert bei Rot.
|
||||
• lucy: deploy/lucy-annahme.sh — Merge im Worktree → Push main → PC baut dist (via PC-Executor)
|
||||
und startet Lucy neu, wenn sie lief → bei Rot Revert auf main (die laufende Lucy bleibt die alte).
|
||||
Detached, weil deploy.sh mission-control-2 neu startet — ein Kind des Backends stürbe mittendrin.
|
||||
Der Fortschritt landet in STATUS_PATH (JSON), das Skript schreibt, die API liest nur.
|
||||
Status-Schlüssel: mc2 = Branch-Name pur (Bestand), lucy = "lucy:<branch>" (kollisionsfrei).
|
||||
|
||||
Lokal (Windows-Dev) ist alles harmlos: available=False, Aktionen geben Fehler statt zu crashen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
REPO = Path(os.environ.get("MC2_SOURCE_DIR", "~/mission-control-v2")).expanduser()
|
||||
LUCY = Path(os.environ.get("MC2_LUCY_DIR", "~/lucy")).expanduser()
|
||||
VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
|
||||
STATUS_PATH = Path(os.environ.get("MC2_AUFTRAG_STATUS", str(MODELS_DIR / "mc2-auftragsbuch.json")))
|
||||
# Empfehlungs-Stempel des nächtlichen Karten-Gutachters (deploy/karten-gutachter.sh schreibt,
|
||||
# die API liest nur) und das Lern-Gedächtnis der Ablehnungen (Radar/Specifier lesen es).
|
||||
GUTACHTEN_PATH = Path(os.environ.get("MC2_KARTEN_GUTACHTEN", str(MODELS_DIR / "mc2-karten-gutachten.json")))
|
||||
ABLEHNUNGEN_PATH = Path(os.environ.get("MC2_ABLEHNUNGEN", str(MODELS_DIR / "mc2-ablehnungen.jsonl")))
|
||||
|
||||
# Karten-Quellen. Die Runner-Skripte liegen IMMER im MC2-Checkout (deploy/) — auch der
|
||||
# Lucy-Runner, denn er läuft auf der Box; nur der Build passiert am PC.
|
||||
REPOS: dict[str, dict] = {
|
||||
"mc2": {"path": REPO, "runner": "auftrag-annehmen.sh", "key": ""},
|
||||
"lucy": {"path": LUCY, "runner": "lucy-annahme.sh", "key": "lucy:"},
|
||||
}
|
||||
|
||||
# Persistenz für gemeldete Branches (Idempotenz: nur EINMAL pro Branch pingen).
|
||||
# Muster wie mc2-announce.json unter MODELS_DIR.
|
||||
ANNOUNCE_BRANCHES_PATH = Path(os.environ.get("MC2_ANNOUNCE_BRANCHES", str(MODELS_DIR / "mc2-announce-branches.json")))
|
||||
|
||||
# Nur diese Branch-Familien sind Vorschläge (main/HEAD & Fremdes bleiben draußen).
|
||||
PREFIXES = ("wartung/", "orchestrator/", "doku/", "feature/")
|
||||
# Branch-Namen kommen vom Client zurück → hart validieren (keine Shell-/Git-Injektion).
|
||||
_BRANCH_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]{0,120}$")
|
||||
_KANDIDAT_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._ -]{0,120}\.md$")
|
||||
|
||||
_fetch_cache: dict = {}
|
||||
_FETCH_EVERY = 30.0 # s — Gitea nicht bei jedem UI-Poll anfragen
|
||||
_CI_CACHE: dict = {} # { (repo, branch, head_sha): {"status": "success", "ts": time} }
|
||||
|
||||
def _gitea_creds() -> tuple | None:
|
||||
# 1. Fallback: Versuch via git credential manager (zukunftsfähig & plattformübergreifend)
|
||||
try:
|
||||
import subprocess
|
||||
p = subprocess.run(
|
||||
["git", "credential", "fill"],
|
||||
input=b"protocol=https\nhost=git.tobisniceshomelab.ddnsfree.com\n",
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
out = p.stdout.decode(errors="replace")
|
||||
u_match = re.search(r"username=(.+)", out)
|
||||
p_match = re.search(r"password=(.+)", out)
|
||||
if u_match and p_match:
|
||||
return u_match.group(1).strip(), p_match.group(1).strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Fallback: Datei (hauptsächlich auf der AI-Box genutzt)
|
||||
try:
|
||||
cred = Path("~/.git-credentials").expanduser()
|
||||
for line in cred.read_text(encoding="utf-8").splitlines():
|
||||
m = re.match(r"https://([^:]+):([^@]+)@git\.tobisniceshomelab", line.strip())
|
||||
if m: return m.group(1), m.group(2)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _fetch_ci_status(repo: str, branch: str, head_sha: str) -> str | None:
|
||||
# Nur auf der Box (wo creds liegen) sinnvoll
|
||||
creds = _gitea_creds()
|
||||
if not creds: return None
|
||||
_user, token = creds
|
||||
cache_key = (repo, branch, head_sha)
|
||||
cached = _CI_CACHE.get(cache_key)
|
||||
if cached and (time.time() - cached["ts"] < 30 or cached["status"] in ("success", "failure", "skipped")):
|
||||
return cached["status"]
|
||||
|
||||
full_repo = "Hitonabi/mission-control-v2" if repo == "mc2" else "Hitonabi/lucy"
|
||||
url = f"http://192.168.178.153:3000/api/v1/repos/{full_repo}/actions/runs?branch={urllib.parse.quote(branch)}&limit=1"
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"Authorization": "token " + token})
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
data = json.load(r)
|
||||
runs = data if isinstance(data, list) else data.get("runs", data.get("workflow_runs", []))
|
||||
if runs:
|
||||
run = runs[0]
|
||||
if run.get("head_sha", "").startswith(head_sha[:7]):
|
||||
st = run.get("status")
|
||||
if st:
|
||||
_CI_CACHE[cache_key] = {"status": st, "ts": time.time()}
|
||||
return st
|
||||
except Exception as e:
|
||||
log.warning("auftragsbuch: CI-Status fetch fehler: %s", e)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _available() -> bool:
|
||||
return os.name == "posix" and (REPO / ".git").exists()
|
||||
|
||||
|
||||
def _repo_ok(repo: str) -> bool:
|
||||
r = REPOS.get(repo)
|
||||
return bool(r) and os.name == "posix" and (r["path"] / ".git").exists()
|
||||
|
||||
|
||||
def _git(repo: str, args: list[str], timeout: int = 20) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(["git", "-C", str(REPOS[repo]["path"]), *args],
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
|
||||
|
||||
def _status_key(repo: str, branch: str) -> str:
|
||||
return f"{REPOS[repo]['key']}{branch}"
|
||||
|
||||
|
||||
def _fetch_throttled(repo: str) -> None:
|
||||
now = time.time()
|
||||
if now - _fetch_cache.get(repo, 0.0) < _FETCH_EVERY:
|
||||
return
|
||||
_fetch_cache[repo] = now
|
||||
try:
|
||||
_git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30)
|
||||
except Exception:
|
||||
log.warning("auftragsbuch: git fetch (%s) fehlgeschlagen", repo, exc_info=True)
|
||||
|
||||
|
||||
def _load_announce_branches() -> set:
|
||||
"""Liefert die Menge der bereits gemeldeten Branch-Namen (Idempotenz)."""
|
||||
try:
|
||||
data = json.loads(ANNOUNCE_BRANCHES_PATH.read_text(encoding="utf-8"))
|
||||
return set(data) if isinstance(data, list) else set()
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def _save_announce_branches(branches: set) -> None:
|
||||
"""Speichert die Menge gemelder Branch-Namen."""
|
||||
try:
|
||||
tmp = ANNOUNCE_BRANCHES_PATH.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(sorted(branches), ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(ANNOUNCE_BRANCHES_PATH)
|
||||
except OSError:
|
||||
log.warning("auftragsbuch: Announce-Branches %s nicht schreibbar", ANNOUNCE_BRANCHES_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _statuses() -> dict:
|
||||
try:
|
||||
return (json.loads(STATUS_PATH.read_text(encoding="utf-8")) or {}).get("branches", {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _set_status(key: str, state: str, detail: str) -> None:
|
||||
try:
|
||||
try:
|
||||
data = json.loads(STATUS_PATH.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
data.setdefault("branches", {})[key] = {"state": state, "detail": detail, "ts": time.time()}
|
||||
tmp = STATUS_PATH.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
tmp.replace(STATUS_PATH)
|
||||
except OSError:
|
||||
log.warning("auftragsbuch: Status %s nicht schreibbar", STATUS_PATH, exc_info=True)
|
||||
|
||||
|
||||
def _gutachten() -> dict:
|
||||
"""Alle Karten-Stempel ("<repo>:<branch>" → {empfehlung, satz, richter, commit_ts, ts})."""
|
||||
try:
|
||||
data = json.loads(GUTACHTEN_PATH.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _remote_branches(repo: str) -> list[str]:
|
||||
r = _git(repo, ["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"])
|
||||
out = []
|
||||
for line in (r.stdout or "").splitlines():
|
||||
name = line.strip().removeprefix("origin/")
|
||||
if name and name != "HEAD" and name.startswith(PREFIXES):
|
||||
out.append(name)
|
||||
return out
|
||||
|
||||
|
||||
def _valid_branch(branch: str) -> bool:
|
||||
return bool(_BRANCH_RX.match(branch)) and ".." not in branch and branch.startswith(PREFIXES)
|
||||
|
||||
|
||||
def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
|
||||
items = []
|
||||
for branch in _remote_branches(repo):
|
||||
ref = f"origin/{branch}"
|
||||
status = statuses.get(_status_key(repo, branch))
|
||||
try:
|
||||
ahead = int((_git(repo, ["rev-list", "--count", f"origin/main..{ref}"]).stdout or "0").strip() or 0)
|
||||
if ahead == 0 and ((status or {}).get("state") not in ("laeuft", "rollback")):
|
||||
continue # bereits in main enthalten → keine offene Entscheidung mehr
|
||||
behind = int((_git(repo, ["rev-list", "--count", f"{ref}..origin/main"]).stdout or "0").strip() or 0)
|
||||
# Kaputt aufgesetzte Branches entlarven (Worker machte git init/Shallow statt zu
|
||||
# klonen): ohne gemeinsamen Vorfahren kann git NIE mergen — Annehmen wäre ein
|
||||
# garantierter Fehllauf („refusing to merge unrelated histories").
|
||||
verwaist = _git(repo, ["merge-base", "origin/main", ref]).returncode != 0
|
||||
show = _git(repo, ["show", "-s", "--format=%s%x1f%b%x1f%ct%x1f%an", ref])
|
||||
subject, body, cts, author = ((show.stdout or "").split("\x1f") + ["", "", "", ""])[:4]
|
||||
ts_val = int(cts) if cts.strip().isdigit() else None
|
||||
# Stempel nur zeigen, wenn er zum AKTUELLEN Commit gehört (nachgeschobener
|
||||
# Commit macht das alte Gutachten ungültig — der Nacht-Lauf stempelt neu).
|
||||
stempel = gutachten.get(f"{repo}:{branch}")
|
||||
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
|
||||
stempel = None
|
||||
|
||||
head_sha = (_git(repo, ["rev-parse", ref]).stdout or "").strip()
|
||||
ci_status = _fetch_ci_status(repo, branch, head_sha) if head_sha else None
|
||||
|
||||
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
|
||||
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
|
||||
files = []
|
||||
for line in files_raw[:60]:
|
||||
parts = line.split("\t")
|
||||
if len(parts) >= 2:
|
||||
files.append({"status": parts[0][:2], "path": parts[-1]})
|
||||
paths = [f["path"] for f in files]
|
||||
frontend_src = any(p.startswith("frontend/src") for p in paths)
|
||||
frontend_dist = any(p.startswith("frontend/dist") for p in paths)
|
||||
items.append({
|
||||
"repo": repo,
|
||||
"branch": branch,
|
||||
"kind": branch.split("/", 1)[0],
|
||||
"subject": subject.strip(),
|
||||
"body": body.strip()[:2000],
|
||||
"author": author.strip(),
|
||||
"ts": ts_val,
|
||||
"empfehlung": stempel,
|
||||
"ahead": ahead,
|
||||
"behind": behind,
|
||||
"verwaist": verwaist,
|
||||
# Diff gegen main ist leer → der Branch brächte nichts (Inhalt schon in main
|
||||
# oder Worker hat am Repo vorbei gearbeitet). Bei verwaist ist der Diff
|
||||
# technisch leer (kein merge-base) — dann zählt nur das verwaist-Flag.
|
||||
"leer": not verwaist and ahead > 0 and not files_raw,
|
||||
"shortstat": stat,
|
||||
"files": files,
|
||||
"files_truncated": len(files_raw) > 60,
|
||||
# Nur mc2: Frontend-Quelltext ohne gebautes Bundle — die Box deployt dist so, wie
|
||||
# es im Repo liegt. Lucy wird dagegen IMMER am PC gebaut (kein dist im Repo).
|
||||
"frontend_ohne_build": repo == "mc2" and frontend_src and not frontend_dist,
|
||||
"status": None if (status or {}).get("state") == "eingespielt" and ahead > 0 else status,
|
||||
"ci_status": ci_status,
|
||||
})
|
||||
except Exception:
|
||||
log.warning("auftragsbuch: Branch %s (%s) nicht lesbar", branch, repo, exc_info=True)
|
||||
return items
|
||||
|
||||
|
||||
def list_proposals() -> dict:
|
||||
"""Alle offenen Vorschläge: Branches aus beiden Repos (mit Commit-/Diff-Zusammenfassung +
|
||||
Status) und Skill-Kandidaten aus dem Vault. Eine Antwort für die ganze Auftragsbuch-Seite.
|
||||
|
||||
Nebenbei: Pinge neu auftauchende Vorschlags-Branches per Telegram und füge sie dem
|
||||
Briefkasten hinzu (Idempotenz: nur EINMAL pro Branch)."""
|
||||
if not _available():
|
||||
return {"available": False, "items": [], "skill_kandidaten": [], "open_count": 0}
|
||||
|
||||
statuses = _statuses()
|
||||
gutachten = _gutachten()
|
||||
items = []
|
||||
for repo in REPOS:
|
||||
if not _repo_ok(repo):
|
||||
continue # z. B. ~/lucy (noch) nicht geklont → Karten dieses Repos einfach weglassen
|
||||
_fetch_throttled(repo)
|
||||
items.extend(_repo_items(repo, statuses, gutachten))
|
||||
items.sort(key=lambda i: i.get("ts") or 0, reverse=True)
|
||||
|
||||
# --- Telegram-Ping für NEUE Vorschlags-Branches (Idempotenz) ---
|
||||
# Alle aktuellen Branch-Namen aus beiden Repos sammeln
|
||||
aktuelle_branches = set()
|
||||
for repo in REPOS:
|
||||
if _repo_ok(repo):
|
||||
aktuelle_branches.update(_remote_branches(repo))
|
||||
|
||||
# Bereits gemeldete Branches laden
|
||||
gemeldet = _load_announce_branches()
|
||||
|
||||
# Nur wirklich NEUE Branches pingen
|
||||
neue = aktuelle_branches - gemeldet
|
||||
for branch in neue:
|
||||
# NotifyTelegram best-effort, niemals crashen
|
||||
try:
|
||||
from services import announce
|
||||
subject = "[Auftragsbuch]"
|
||||
text = f"Neue Karte wartet auf deinen Klick: {branch}"
|
||||
announce.notify_telegram(subject, text)
|
||||
# Und ein Eintrag für den Briefkasten (damit Lucy es spricht)
|
||||
announce.add(text, subject, "auftragsbuch", "normal")
|
||||
except Exception:
|
||||
log.warning("auftragsbuch: Telegram-Ping für %s fehlgeschlagen", branch, exc_info=True)
|
||||
|
||||
# Als gemeldet speichern — aber NUR bei echter Änderung. Ein unbedingtes Schreiben
|
||||
# bumpte die mtime bei jedem Aufruf, der SSE-Sammler (events.py, Fingerabdruck =
|
||||
# Datei-mtime) meldete daraufhin „geändert", die UI holte neu, schrieb wieder …
|
||||
# → 3-s-Endlosschleife auf /api/auftragsbuch je offenem Client (gefunden 15.07. nachts).
|
||||
if aktuelle_branches != gemeldet:
|
||||
_save_announce_branches(aktuelle_branches)
|
||||
|
||||
kandidaten = list_skill_kandidaten()
|
||||
open_count = sum(1 for i in items
|
||||
if (i.get("status") or {}).get("state") not in ("eingespielt",)) + len(kandidaten)
|
||||
return {"available": True, "items": items, "skill_kandidaten": kandidaten, "open_count": open_count}
|
||||
|
||||
|
||||
def diff_of(branch: str, repo: str = "mc2") -> dict:
|
||||
if not _repo_ok(repo):
|
||||
return {"ok": False, "error": "Nur auf der Box verfügbar."}
|
||||
if not _valid_branch(branch) or branch not in _remote_branches(repo):
|
||||
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
|
||||
r = _git(repo, ["diff", f"origin/main...origin/{branch}"], timeout=30)
|
||||
text = r.stdout or ""
|
||||
truncated = len(text) > 200_000
|
||||
return {"ok": True, "diff": text[:200_000], "truncated": truncated}
|
||||
|
||||
|
||||
def accept(branch: str, repo: str = "mc2") -> dict:
|
||||
"""Annehmen: detached Runner starten. mc2: Merge→Push→Deploy→Health→ggf. Rollback.
|
||||
lucy: Merge→Push→PC-Build+Neustart→ggf. Revert (der Runner spricht den PC-Executor an)."""
|
||||
if not _repo_ok(repo):
|
||||
return {"ok": False, "error": "Annehmen geht nur auf der Box."}
|
||||
if not _valid_branch(branch) or branch not in _remote_branches(repo):
|
||||
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
|
||||
key = _status_key(repo, branch)
|
||||
state = (_statuses().get(key) or {}).get("state")
|
||||
if state in ("laeuft", "rollback"):
|
||||
return {"ok": False, "error": "Für diesen Vorschlag läuft bereits ein Annahme-Lauf."}
|
||||
# Kaputt aufgesetzter Branch (kein gemeinsamer Vorfahre) → Merge kann NIE gelingen;
|
||||
# gar nicht erst einen Runner starten (die UI blendet den Knopf aus, die API hält dicht).
|
||||
if _git(repo, ["merge-base", "origin/main", f"origin/{branch}"]).returncode != 0:
|
||||
return {"ok": False, "error": "Dieser Branch hat keinen gemeinsamen Ursprung mit main "
|
||||
"(kaputt aufgesetzt, z. B. git init statt Klonen) — Annehmen ist technisch unmöglich. "
|
||||
"Bitte ablehnen (gern mit Grund) und die Idee neu in die Queue geben."}
|
||||
|
||||
# Runner als /tmp-Kopie starten: deploy.sh resettet das Repo hart — das Original-Skript
|
||||
# würde einem laufenden bash unter den Füßen getauscht (bekannte Selbst-Reset-Falle).
|
||||
src = REPO / "deploy" / REPOS[repo]["runner"]
|
||||
if not src.is_file():
|
||||
return {"ok": False, "error": f"Runner fehlt im Checkout: {src.name}"}
|
||||
runner = Path(f"/tmp/mc2-auftrag-runner-{int(time.time())}.sh")
|
||||
try:
|
||||
shutil.copyfile(src, runner)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "error": f"Runner nicht kopierbar: {exc}"}
|
||||
|
||||
slug = re.sub(r"[^a-z0-9-]+", "-", f"{repo}-{branch}".lower())[:40].strip("-")
|
||||
unit = f"mc2-auftrag-{slug}-{int(time.time())}"
|
||||
r = subprocess.run(
|
||||
["systemd-run", "--user", "--collect", f"--unit={unit}",
|
||||
"/bin/bash", str(runner), branch],
|
||||
capture_output=True, text=True, timeout=20)
|
||||
if r.returncode != 0:
|
||||
return {"ok": False, "error": f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}"}
|
||||
_set_status(key, "laeuft", "Annahme-Lauf gestartet")
|
||||
return {"ok": True, "unit": unit}
|
||||
|
||||
|
||||
def reject(branch: str, repo: str = "mc2", grund: str = "") -> dict:
|
||||
"""Ablehnen: Remote-Branch löschen (die Arbeit bleibt in der Gitea-Historie referenzierbar,
|
||||
aber die Entscheidung ist gefallen). Der optionale GRUND ist das Lern-Gedächtnis des
|
||||
Kreislaufs: er landet in ABLEHNUNGEN_PATH (jsonl), und Idle-Radar/Analysten bekommen
|
||||
ihn als „NIE wieder vorschlagen"-Material vorgelegt. Meldung in den Briefkasten."""
|
||||
if not _repo_ok(repo):
|
||||
return {"ok": False, "error": "Ablehnen geht nur auf der Box."}
|
||||
if not _valid_branch(branch) or branch not in _remote_branches(repo):
|
||||
return {"ok": False, "error": f"Unbekannter Vorschlags-Branch: {branch}"}
|
||||
grund = (grund or "").strip()[:400]
|
||||
# Betreff VOR dem Löschen sichern — danach ist der Ref weg.
|
||||
subject = (_git(repo, ["show", "-s", "--format=%s", f"origin/{branch}"]).stdout or "").strip()[:200]
|
||||
r = _git(repo, ["push", "origin", "--delete", branch], timeout=30)
|
||||
if r.returncode != 0:
|
||||
return {"ok": False, "error": f"Löschen fehlgeschlagen: {(r.stderr or '').strip()[:300]}"}
|
||||
_git(repo, ["fetch", "-q", "--prune", "origin"], timeout=30)
|
||||
detail = "Vom Commander abgelehnt — Branch gelöscht"
|
||||
if grund:
|
||||
detail += f" (Grund: {grund})"
|
||||
_set_status(_status_key(repo, branch), "abgelehnt", detail)
|
||||
try:
|
||||
with ABLEHNUNGEN_PATH.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"ts": int(time.time()), "repo": repo, "branch": branch,
|
||||
"subject": subject, "grund": grund}, ensure_ascii=False) + "\n")
|
||||
except OSError:
|
||||
log.warning("auftragsbuch: Ablehn-Gedächtnis %s nicht schreibbar", ABLEHNUNGEN_PATH, exc_info=True)
|
||||
try:
|
||||
from services import announce
|
||||
wo = "im Lucy-Repo " if repo == "lucy" else ""
|
||||
warum = f" Grund: {grund}" if grund else ""
|
||||
announce.add(f"Vorschlag '{branch}' {wo}wurde abgelehnt und der Branch gelöscht.{warum}",
|
||||
"[Auftragsbuch]", "auftragsbuch", "silent")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Skill-Kandidaten (Wissens-Vault) ─────────────────────────────────────────
|
||||
|
||||
def _kandidaten_dir() -> Path:
|
||||
return VAULT / "skill-kandidaten"
|
||||
|
||||
|
||||
def list_skill_kandidaten() -> list[dict]:
|
||||
d = _kandidaten_dir()
|
||||
if os.name != "posix" or not d.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for p in sorted(d.glob("*.md"), key=lambda x: x.stat().st_mtime, reverse=True):
|
||||
try:
|
||||
text = p.read_text(encoding="utf-8", errors="replace")
|
||||
first = next((ln.strip().lstrip("# ") for ln in text.splitlines() if ln.strip()), p.stem)
|
||||
out.append({"file": p.name, "title": first[:160],
|
||||
"preview": text[:3000], "mtime": p.stat().st_mtime})
|
||||
except OSError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _vault_git(args: list[str]) -> None:
|
||||
try:
|
||||
subprocess.run(["git", "-C", str(VAULT), *args], capture_output=True, text=True, timeout=15)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _kandidat_path(fname: str) -> Path | None:
|
||||
if not _KANDIDAT_RX.match(fname):
|
||||
return None
|
||||
p = (_kandidaten_dir() / fname).resolve()
|
||||
if not p.is_relative_to(_kandidaten_dir().resolve()) or not p.is_file():
|
||||
return None
|
||||
return p
|
||||
|
||||
|
||||
def skill_accept(fname: str) -> dict:
|
||||
"""Skill-Kandidat beauftragen: Inhalt als Werkstatt-Auftrag an die bewährte
|
||||
`hermes -z`-CLI-Lane (detached — der Lauf dauert Minuten und braucht das Backend nicht).
|
||||
Ergebnis ist wieder propose-only: ein neuer Branch, der hier als Karte auftaucht."""
|
||||
if not _available():
|
||||
return {"ok": False, "error": "Beauftragen geht nur auf der Box."}
|
||||
p = _kandidat_path(fname)
|
||||
if not p:
|
||||
return {"ok": False, "error": f"Kandidat nicht gefunden: {fname}"}
|
||||
|
||||
beauftragt = _kandidaten_dir() / "beauftragt"
|
||||
beauftragt.mkdir(parents=True, exist_ok=True)
|
||||
target = beauftragt / p.name
|
||||
try:
|
||||
content = p.read_text(encoding="utf-8", errors="replace")
|
||||
p.rename(target)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "error": f"Kandidat nicht verschiebbar: {exc}"}
|
||||
_vault_git(["add", "-A"])
|
||||
_vault_git(["commit", "-q", "-m", f"Skill-Kandidat beauftragt: {p.name}"])
|
||||
|
||||
order = ("Nutze den orchestrator-Skill (propose-only, Zwei-Kritiker-Gate). "
|
||||
"Auftrag aus dem Wissens-Vault (vom Commander im Auftragsbuch freigegeben):\n\n"
|
||||
+ content)
|
||||
order_file = Path(f"/tmp/mc2-skill-auftrag-{int(time.time())}.txt")
|
||||
try:
|
||||
order_file.write_text(order, encoding="utf-8")
|
||||
except OSError as exc:
|
||||
return {"ok": False, "error": f"Auftrag nicht schreibbar: {exc}"}
|
||||
unit = f"mc2-skill-auftrag-{int(time.time())}"
|
||||
r = subprocess.run(
|
||||
["systemd-run", "--user", "--collect", f"--unit={unit}",
|
||||
"/bin/bash", "-lc", f'hermes -z "$(cat {order_file})"'],
|
||||
capture_output=True, text=True, timeout=20)
|
||||
if r.returncode != 0:
|
||||
return {"ok": False, "error": f"Start fehlgeschlagen: {(r.stderr or r.stdout).strip()[:300]}"}
|
||||
try:
|
||||
from services import announce
|
||||
announce.add(f"Skill-Kandidat '{fname}' wurde beauftragt — die Werkstatt arbeitet, "
|
||||
"das Ergebnis erscheint als neuer Vorschlag im Auftragsbuch.",
|
||||
"[Auftragsbuch]", "auftragsbuch", "silent")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "unit": unit}
|
||||
|
||||
|
||||
def skill_reject(fname: str) -> dict:
|
||||
if not _available():
|
||||
return {"ok": False, "error": "Verwerfen geht nur auf der Box."}
|
||||
p = _kandidat_path(fname)
|
||||
if not p:
|
||||
return {"ok": False, "error": f"Kandidat nicht gefunden: {fname}"}
|
||||
verworfen = _kandidaten_dir() / "verworfen"
|
||||
verworfen.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
p.rename(verworfen / p.name)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "error": f"Kandidat nicht verschiebbar: {exc}"}
|
||||
_vault_git(["add", "-A"])
|
||||
_vault_git(["commit", "-q", "-m", f"Skill-Kandidat verworfen: {p.name}"])
|
||||
return {"ok": True}
|
||||
@@ -1,7 +1,13 @@
|
||||
"""
|
||||
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
||||
LOKALEN PC (separate Maschine im LAN). Alle zeigen auf den **Gateway** der Box
|
||||
(reale Modelle coder/heavy/vision, Cockpit-Port :9001/v1) + den **Shared-Memory-MCP** (MC :9001).
|
||||
(reale Modelle coder/heavy/vision, Cockpit-Port :9001/v1).
|
||||
|
||||
Gedächtnis ist hier bewusst KEINE eigene Leitung mehr: bis August 2026 gab es dafür einen
|
||||
Memory-MCP-Server gegen MC2s /api/memory; seit dessen Ablösung (docs/wissen/VERDIKTE.md,
|
||||
07.08.2026) führt Hermes sein Gedächtnis
|
||||
selbst — es hängt am Agenten, nicht am Gateway. check_health() prüft es weiterhin (Leitung 2),
|
||||
es ist nur nichts mehr zu konfigurieren.
|
||||
|
||||
Wichtig: Host ist die LAN-IP der Box (NICHT eine Proxy-Domain) — das war in v1
|
||||
die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
||||
@@ -40,9 +46,7 @@ def _gw(host: str) -> str:
|
||||
return f"http://{host}:{PORT}/v1"
|
||||
|
||||
|
||||
def build_snippets(host: str = DEFAULT_HOST,
|
||||
mcp_script_path: str = r"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py",
|
||||
mcp_python: str = "python") -> dict:
|
||||
def build_snippets(host: str = DEFAULT_HOST) -> dict:
|
||||
gw = _gw(host)
|
||||
mc_url = f"http://{host}:{PORT}"
|
||||
|
||||
@@ -88,16 +92,6 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
f'export ANTHROPIC_MODEL="coder"'
|
||||
)
|
||||
|
||||
memory_mcp = json.dumps({
|
||||
"mcpServers": {
|
||||
"mission-control-memory": {
|
||||
"command": mcp_python,
|
||||
"args": [mcp_script_path],
|
||||
"env": {"MC_URL": mc_url},
|
||||
}
|
||||
}
|
||||
}, indent=2)
|
||||
|
||||
return {
|
||||
"host": host,
|
||||
"gateway_url": gw,
|
||||
@@ -116,11 +110,8 @@ def build_snippets(host: str = DEFAULT_HOST,
|
||||
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
||||
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
||||
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
|
||||
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway; Gedächtnis-Anbindung via Memory-MCP unten."},
|
||||
"note": "Für die starken Sessions. Lokal-Betrieb bräuchte einen Anthropic⇄OpenAI-Übersetzer vor dem Gateway."},
|
||||
},
|
||||
# Leitung 2 — das GEDÄCHTNIS. Separater MCP-Server, gilt zusätzlich zu jedem Tool oben.
|
||||
"memory": {"label": "Shared Memory (MCP)", "lang": "json", "snippet": memory_mcp,
|
||||
"note": "Eigene Leitung: MCP-Block für jedes MCP-fähige Tool. mcp_memory.py muss lokal liegen."},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Geheimnis-Ablage auf der Box (v3-Umbau P1).
|
||||
|
||||
WARUM ES DAS GIBT: Bis zum 28.08.2026 lagen das Box-Sudo-Passwort und der
|
||||
HuggingFace-Token im `localStorage` des Browsers und reisten bei jedem mutierenden
|
||||
Request mit (Header `X-Sudo-Password` **und** im JSON-Rumpf). Da der Dienst-Nutzer
|
||||
laut `/etc/sudoers` mit `NOPASSWD: ALL` läuft, wäre ein einziger XSS in der SPA
|
||||
gleichbedeutend mit Root auf der Box gewesen.
|
||||
|
||||
Das Sudo-Passwort ist ersatzlos entfallen — auf der Box gemessen: `sudo -n true`
|
||||
läuft durch, es wurde also nie gebraucht. Bleibt der HF-Token; der liegt jetzt hier:
|
||||
eine Datei neben den anderen `mc2-*.json` unter MODELS_DIR, Rechte 0600, und er wird
|
||||
**nie** an den Browser zurückgegeben. Die Oberfläche erfährt nur, OB einer gesetzt ist.
|
||||
|
||||
Absichtlich kein Verschlüsseln: Der Schlüssel müsste auf derselben Maschine liegen und
|
||||
wäre damit Theater. Der Gewinn ist, dass das Geheimnis den Browser gar nicht erst
|
||||
erreicht — nicht, dass die Datei unlesbar wäre.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from config import MODELS_DIR
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
PFAD = Path(os.environ.get("MC_GEHEIMNISSE_PFAD", str(MODELS_DIR / "mc2-geheimnisse.json")))
|
||||
|
||||
# Was hier abgelegt werden darf. Neue Schlüssel bewusst eintragen — so kann ein
|
||||
# fehlgeleiteter Request keine beliebigen Felder in die Datei schreiben.
|
||||
ERLAUBT = frozenset({"hf_token"})
|
||||
|
||||
|
||||
def _lesen() -> dict[str, str]:
|
||||
"""Ganze Ablage. Fehlt die Datei (frische Box, Windows-Entwicklungsrechner ohne
|
||||
/srv/models), ist das kein Fehler, sondern schlicht 'nichts gesetzt'."""
|
||||
try:
|
||||
daten = json.loads(PFAD.read_text(encoding="utf-8"))
|
||||
return {k: v for k, v in daten.items() if k in ERLAUBT and isinstance(v, str)}
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def _schreiben(daten: dict[str, str]) -> bool:
|
||||
"""Atomar über eine Nachbardatei, damit ein Absturz mittendrin keine halbe Datei
|
||||
hinterlässt. Rechte 0600 werden VOR dem Umbenennen gesetzt — sonst gäbe es ein
|
||||
Zeitfenster, in dem das Geheimnis world-readable auf der Platte liegt."""
|
||||
try:
|
||||
PFAD.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = PFAD.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(daten, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
try:
|
||||
os.chmod(tmp, 0o600)
|
||||
except OSError:
|
||||
pass # Windows kennt keine Unix-Rechte — lokal harmlos, auf der Box greift es
|
||||
tmp.replace(PFAD)
|
||||
return True
|
||||
except OSError as exc:
|
||||
log.warning("Geheimnis-Ablage nicht schreibbar (%s): %s", PFAD, exc)
|
||||
return False
|
||||
|
||||
|
||||
def hf_token() -> str | None:
|
||||
"""Der HF-Token für Modell-Downloads. Reihenfolge: Prozess-Env schlägt Datei —
|
||||
so kann die systemd-Unit ihn setzen, ohne dass jemand die Oberfläche anfassen muss."""
|
||||
return os.environ.get("HF_TOKEN") or _lesen().get("hf_token") or None
|
||||
|
||||
|
||||
def setzen(schluessel: str, wert: str | None) -> bool:
|
||||
"""Setzt oder löscht (wert=None oder leer) ein Geheimnis."""
|
||||
if schluessel not in ERLAUBT:
|
||||
return False
|
||||
daten = _lesen()
|
||||
if wert:
|
||||
daten[schluessel] = wert
|
||||
else:
|
||||
daten.pop(schluessel, None)
|
||||
return _schreiben(daten)
|
||||
|
||||
|
||||
def status() -> dict[str, bool]:
|
||||
"""Was die Oberfläche erfahren darf: nur, OB etwas gesetzt ist — nie der Wert.
|
||||
`aus_env` sagt dem Nutzer, warum ein Löschen in der Oberfläche wirkungslos bliebe."""
|
||||
daten = _lesen()
|
||||
return {
|
||||
"hf_token_gesetzt": bool(daten.get("hf_token") or os.environ.get("HF_TOKEN")),
|
||||
"hf_token_aus_env": bool(os.environ.get("HF_TOKEN")),
|
||||
"schreibbar": _schreibbar(),
|
||||
}
|
||||
|
||||
|
||||
def _schreibbar() -> bool:
|
||||
"""Ehrlich melden, wenn die Ablage nicht beschreibbar ist (z. B. lokal auf Windows
|
||||
ohne /srv/models) — sonst speichert die Oberfläche scheinbar erfolgreich ins Leere."""
|
||||
try:
|
||||
PFAD.parent.mkdir(parents=True, exist_ok=True)
|
||||
return os.access(PFAD.parent, os.W_OK)
|
||||
except OSError:
|
||||
return False
|
||||
@@ -57,39 +57,29 @@ def _pump_output(job: dict, stream) -> None:
|
||||
commit()
|
||||
|
||||
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None, sudo_password: str | None = None):
|
||||
def _run_job(job_id: str, args: list[str], env: dict | None = None):
|
||||
"""Job-Prozess starten und mitschreiben.
|
||||
|
||||
v3-Umbau P1 (28.08.2026): Hier wurde frueher ein Sudo-Passwort aus dem Browser an
|
||||
stdin gefuettert (und dafuer `sudo -n` in den Argumenten zu `sudo -S` umgeschrieben).
|
||||
Auf der Box laeuft sudo passwortlos (`NOPASSWD: ALL`), der Pfad war tot. Ohne ihn
|
||||
braucht der Prozess auch keine stdin-Pipe mehr: DEVNULL sorgt dafuer, dass ein Job,
|
||||
der wider Erwarten nach einem Passwort fragt, sofort scheitert statt still zu haengen."""
|
||||
job = JOBS[job_id]
|
||||
job["state"] = "running"
|
||||
try:
|
||||
actual_args = list(args)
|
||||
if sudo_password is not None:
|
||||
for i, arg in enumerate(actual_args):
|
||||
if isinstance(arg, str):
|
||||
actual_args[i] = arg.replace("sudo -n", "sudo -S").replace("sudo ", "sudo -S ")
|
||||
|
||||
proc = subprocess.Popen(
|
||||
actual_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.PIPE if sudo_password is not None else None,
|
||||
list(args), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.DEVNULL,
|
||||
bufsize=0,
|
||||
env={**os.environ, **(env or {})},
|
||||
)
|
||||
_PROCS[job_id] = proc
|
||||
|
||||
if sudo_password is not None and proc.stdin:
|
||||
proc.stdin.write((sudo_password + "\n").encode("utf-8"))
|
||||
proc.stdin.flush()
|
||||
proc.stdin.close()
|
||||
|
||||
_pump_output(job, proc.stdout)
|
||||
proc.wait()
|
||||
job["returncode"] = proc.returncode
|
||||
job["state"] = "canceled" if job.get("canceled") else ("done" if proc.returncode == 0 else "failed")
|
||||
|
||||
# Check if failed due to sudo authorization failure
|
||||
if proc.returncode != 0 and job["log"]:
|
||||
log_str = "\n".join(job["log"])
|
||||
if "a password is required" in log_str or "password" in log_str.lower() or "sudo:" in log_str:
|
||||
job["sudo_failed"] = True
|
||||
except Exception as exc:
|
||||
_append_log(job, f"[mc] Fehler: {exc}")
|
||||
job["state"] = "failed"
|
||||
@@ -148,9 +138,8 @@ def attach_download_progress(job_id: str, local_dir: str, total_bytes: int) -> N
|
||||
|
||||
|
||||
def start_job(args: list[str], label: str, env: dict | None = None, on_done=None,
|
||||
sudo_password: str | None = None, group: str | None = None) -> str:
|
||||
group: str | None = None) -> str:
|
||||
job_id = uuid.uuid4().hex[:12]
|
||||
# Mask password in log if present in args
|
||||
log_args = list(args)
|
||||
JOBS[job_id] = {
|
||||
"id": job_id, "label": label, "state": "queued", "group": group,
|
||||
@@ -159,7 +148,7 @@ def start_job(args: list[str], label: str, env: dict | None = None, on_done=None
|
||||
}
|
||||
if on_done:
|
||||
JOBS[job_id]["_on_done"] = on_done
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env, sudo_password), daemon=True).start()
|
||||
threading.Thread(target=_run_job, args=(job_id, args, env), daemon=True).start()
|
||||
return job_id
|
||||
|
||||
|
||||
|
||||
+806
-802
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import time
|
||||
|
||||
import httpx
|
||||
import psutil
|
||||
from config import HERMES_API_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
from config import HERMES_API_KEY, HERMES_API_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||
|
||||
from services import announce, llamaswap
|
||||
|
||||
@@ -34,14 +34,26 @@ REMIND_S = int(os.environ.get("MC_SENTRY_REMIND_S", "21600")) # Erinnerung
|
||||
DISK_ALARM_PCT = float(os.environ.get("MC_SENTRY_DISK_PCT", "90"))
|
||||
|
||||
|
||||
def _reach(url: str, path: str = "/health") -> bool:
|
||||
def _reach(url: str, path: str = "/health", headers: dict[str, str] | None = None) -> bool:
|
||||
"""Antwortet der Dienst ueberhaupt? `< 500` ist bewusst nachsichtig: Die Frage ist
|
||||
"laeuft er", nicht "darf ich rein" — ein 401 beweist, dass jemand zuhoert.
|
||||
|
||||
`headers` gibt es seit dem 28.08.2026: Die Hermes-Probe schlug ohne API-Schluessel an
|
||||
und erzeugte dabei 720 `rejected invalid API key`-Warnungen pro Tag in dessen Log
|
||||
(gemessen: 30/Stunde seit dem 27.08. 15:05). Das Urteil war richtig, der Laerm nicht —
|
||||
und er haette einen echten Auth-Fehler unter sich begraben."""
|
||||
try:
|
||||
with httpx.Client(timeout=5.0) as c:
|
||||
return c.get(f"{url}{path}").status_code < 500
|
||||
return c.get(f"{url}{path}", headers=headers or {}).status_code < 500
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _hermes_kopf() -> dict[str, str]:
|
||||
"""Bearer-Kopf fuer die Hermes-Platform, falls ein Schluessel konfiguriert ist."""
|
||||
return {"Authorization": f"Bearer {HERMES_API_KEY}"} if HERMES_API_KEY else {}
|
||||
|
||||
|
||||
def _check_engine() -> bool:
|
||||
return llamaswap.engine_reachable()
|
||||
|
||||
@@ -70,7 +82,7 @@ CHECKS: dict[str, tuple] = {
|
||||
"brain": (_check_brain,
|
||||
"Mein Gehirn lädt nicht — ich kann gerade nicht richtig denken. Ein Neustart der Engine könnte helfen.",
|
||||
"Mein Gehirn ist wieder geladen. Alles klar bei mir."),
|
||||
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models"),
|
||||
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models", _hermes_kopf()),
|
||||
"Der Agent-Dienst ist ausgefallen — Telegram und meine Tools gehen gerade nicht.",
|
||||
"Der Agent-Dienst läuft wieder."),
|
||||
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
||||
@@ -89,7 +101,7 @@ if os.environ.get("MC_SENTRY_WATCH_MC2", "") == "1":
|
||||
_MC2_URL = os.environ.get("MC_SENTRY_MC2_URL", "http://127.0.0.1:9001")
|
||||
_GW_URL = os.environ.get("MC_SENTRY_GATEWAY_URL", "http://127.0.0.1:9010")
|
||||
CHECKS["mc2"] = (lambda: _reach(_MC2_URL, "/api/health"),
|
||||
"Das Steuerpult ist ausgefallen — Dashboard, Briefkasten und Auftragsbuch gehen gerade nicht.",
|
||||
"Das Steuerpult ist ausgefallen — Dashboard und Briefkasten gehen gerade nicht.",
|
||||
"Das Steuerpult ist wieder da.")
|
||||
CHECKS["gateway"] = (lambda: _reach(_GW_URL, "/gw/health"),
|
||||
"Der Modell-Gateway ist ausgefallen — meine Denk-Anfragen und die der Worker hängen gerade.",
|
||||
|
||||
@@ -10,6 +10,7 @@ import glob
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
import psutil
|
||||
from config import MODELS_DIR
|
||||
@@ -197,5 +198,59 @@ def system_status() -> dict:
|
||||
"gpu": _gpu_sysfs(),
|
||||
"temp": _temps(),
|
||||
"disk": disk,
|
||||
# Betriebszeit in Sekunden (v3-Umbau P3). Gehoert in die neue Statusleiste, weil
|
||||
# sich die Box woechentlich selbst neu startet, wenn das OS es verlangt — dann ist
|
||||
# "laeuft seit 20 Minuten" die Antwort auf eine ganze Klasse von Fragen.
|
||||
"uptime_s": _uptime_s(),
|
||||
"versions": check_versions_cached(),
|
||||
}
|
||||
|
||||
|
||||
def metrik_punkt() -> dict:
|
||||
"""Leichter Messpunkt fuer den Ereignisstrom (v3-Umbau P4) — EINMAL pro Sekunde.
|
||||
|
||||
Bewusst NICHT `system_status()`: das ruft `psutil.cpu_percent(interval=0.1)` und
|
||||
blockiert damit den Event-Loop 100 ms je Aufruf (bei 1-s-Takt also 10 % der Zeit),
|
||||
und es haengt den Versions-Check dran, den niemand sekuendlich braucht.
|
||||
|
||||
`interval=None` misst gegen den VORIGEN Aufruf statt zu warten — genau richtig fuer
|
||||
einen festen Takt. Der allererste Wert ist 0.0; das faellt bei 1 s nicht auf.
|
||||
|
||||
Token stehen hier als GESAMTZAEHLER, nicht als Rate: Der Klient rechnet die Rate aus
|
||||
zwei Punkten selbst. So bleibt der Server zustandslos und ein verpasster Punkt
|
||||
verfaelscht nichts."""
|
||||
vm = psutil.virtual_memory()
|
||||
temp = _temps() or {}
|
||||
gpu = _gpu_sysfs() or {}
|
||||
try:
|
||||
from services.token_stats import get_stats
|
||||
tok = get_stats()
|
||||
except Exception:
|
||||
tok = {}
|
||||
try:
|
||||
du = psutil.disk_usage(str(MODELS_DIR) if MODELS_DIR.exists() else os.getcwd())
|
||||
disk = du.percent
|
||||
except Exception:
|
||||
disk = None
|
||||
return {
|
||||
"cpu": psutil.cpu_percent(interval=None),
|
||||
"ram": vm.percent,
|
||||
"ram_used": vm.used,
|
||||
"ram_total": vm.total,
|
||||
"gpu": gpu.get("busy_percent"),
|
||||
"disk": disk,
|
||||
"temp_cpu": temp.get("cpu"),
|
||||
"temp_gpu": temp.get("gpu"),
|
||||
"uptime_s": _uptime_s(),
|
||||
"tok_p": tok.get("prompt_tokens", 0),
|
||||
"tok_c": tok.get("completion_tokens", 0),
|
||||
}
|
||||
|
||||
|
||||
def _uptime_s() -> int | None:
|
||||
"""Sekunden seit dem Systemstart. None statt einer Ausrede, wenn psutil hier nichts
|
||||
liefert — eine erfundene Zahl waere schlimmer als eine fehlende."""
|
||||
try:
|
||||
return int(time.time() - psutil.boot_time())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -11,15 +11,14 @@ Zwei Sichten auf dieselben Messungen:
|
||||
|
||||
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
||||
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
||||
**Gedächtnis-Retrieve** läuft zwar in Hermes, ruft aber MC2s Memory-Endpoint per HTTP zurück → messbar
|
||||
und als Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden**
|
||||
dagegen laufen im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im
|
||||
„Generierung"-Bucket.
|
||||
**Gedächtnis-Abruf** dagegen läuft seit der Ablösung des Sidecars (07.08.2026) Hermes-intern — es
|
||||
gibt keinen Rückruf an MC2 mehr, also auch keine Messung; er steckt jetzt in der Hirn-Zeit. Ebenso die
|
||||
**Tool-Runden**: im Hermes-LLM-Loop ohne Callback an MC2 → unsichtbar, im „Generierung"-Bucket.
|
||||
|
||||
STT (davor) und Gedächtnis-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
||||
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
||||
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
||||
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
||||
STT läuft als eigener HTTP-Request VOR dem Turn, ohne Turn-ID. Auf einem EIN-Nutzer-Gerät genügt eine
|
||||
schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer wird global „geparkt" und vom
|
||||
nächsten Chat-Turn eingesammelt (mit Frist-Check). Kein Turn-ID-Durchreichen durch den Lucy-Client
|
||||
nötig.
|
||||
|
||||
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
||||
"""
|
||||
@@ -35,11 +34,11 @@ _STAGES: dict[str, deque] = {}
|
||||
_TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-Trace)
|
||||
|
||||
# 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", "chat_ttfb", "chat_first_content", "tts")
|
||||
|
||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Gedächtnis-Retrieve, je
|
||||
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer als
|
||||
# (ms, perf_counter-Zeitstempel). Der nächste Chat-Turn sammelt sie ein und leert sie.
|
||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None}
|
||||
|
||||
|
||||
def record_stage(stage: str, ms: float) -> None:
|
||||
@@ -54,8 +53,8 @@ def record_stage(stage: str, ms: float) -> None:
|
||||
|
||||
|
||||
def park(kind: str, ms: float) -> None:
|
||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Gedächtnis-Retrieve als
|
||||
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT läuft davor), global parken,
|
||||
damit der nächste Chat-Turn sie einsammeln kann."""
|
||||
if ms is None or ms < 0 or kind not in _PARKED:
|
||||
return
|
||||
with _LOCK:
|
||||
@@ -72,17 +71,6 @@ def _take_stt(max_age: float = 20.0) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
||||
"""Geparkten Gedächtnis-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
||||
Turns) und frisch ist."""
|
||||
with _LOCK:
|
||||
v = _PARKED.get("retrieve")
|
||||
if v and v[1] >= since_perf and (time.perf_counter() - v[1]) <= max_age:
|
||||
_PARKED["retrieve"] = None
|
||||
return round(v[0], 1)
|
||||
return None
|
||||
|
||||
|
||||
class Timer:
|
||||
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
||||
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
||||
@@ -103,8 +91,7 @@ class TurnTrace:
|
||||
"""Ein Per-Turn-Trace für den Voice/Lucy-Chatpfad. In `voice.py` über die Dauer eines Chat-Turns
|
||||
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
||||
|
||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
||||
Unter-Detail: memory (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen."""
|
||||
|
||||
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||
self.id = uuid.uuid4().hex[:8]
|
||||
@@ -139,7 +126,6 @@ class TurnTrace:
|
||||
def commit(self) -> dict:
|
||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
||||
memory = _take_retrieve(self.perf0) # Gedächtnis-Retrieve (HTTP-Rückruf), best-effort
|
||||
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
||||
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||
rec = {
|
||||
@@ -152,7 +138,6 @@ class TurnTrace:
|
||||
"vision_ms": self.vision_ms,
|
||||
"hirn_ms": self.hirn_ms,
|
||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||
"memory_ms": memory,
|
||||
"total_ms": round(total, 1),
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
+3
-1
@@ -10,8 +10,10 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
||||
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
||||
In-Process-Nudge aus llamaswap.write_config)
|
||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||
|
||||
Das dritte Loop (Gedächtnis-Dubletten gegen den Sidecar auf :8765) ist mit dessen Ablösung
|
||||
am 07.08.2026 entfallen — Hermes führt sein Gedächtnis selbst (docs/wissen/VERDIKTE.md).
|
||||
|
||||
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
|
||||
|
||||
@@ -28,7 +28,7 @@ hint=""
|
||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||
ctx="$(cat <<EOF
|
||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642. 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. Weiterer Dienst: hermes-gateway :8642 (fuehrt sein Gedaechtnis selbst — es gibt KEINEN separaten Gedaechtnis-Dienst mehr). Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||
|
||||
@@ -67,6 +67,22 @@ jobs:
|
||||
(cd "$d" && npm ci --no-audit --no-fund) || { rot=1; continue; }
|
||||
if grep -q '"build"' "$pkg"; then (cd "$d" && npm run build) || rot=1; fi
|
||||
if grep -q '"test"' "$pkg"; then (cd "$d" && npm test --silent) || rot=1; fi
|
||||
|
||||
# Buendel-Budget: das Start-Chunk ist, was der Nutzer VOR dem ersten Bild laedt.
|
||||
# Ohne Deckel waechst es unbemerkt zurueck (eine eifrig geladene Diagramm-Lib
|
||||
# reicht). Gemessen wird der frische Build, nicht das committete dist.
|
||||
if [ -d "$d/dist/assets" ]; then
|
||||
# Einstiegs-Chunk aus index.html lesen, nicht per Glob raten: Rollup nennt
|
||||
# auch kleine geteilte Module "index-*.js", und dann misst der Glob den
|
||||
# falschen — das Budget waere still immer gruen.
|
||||
einstieg=$(grep -o 'assets/index-[A-Za-z0-9_-]*\.js' "$d/dist/index.html" | head -1)
|
||||
haupt="$d/$einstieg"
|
||||
if [ -n "$haupt" ]; then
|
||||
gz=$(gzip -c "$haupt" | wc -c)
|
||||
echo "-- Start-Chunk: $gz B gzip (Budget 200000)"
|
||||
[ "$gz" -gt 200000 ] && { echo "❌ Start-Buendel ueber Budget — hinter lazy() legen."; rot=1; }
|
||||
fi
|
||||
fi
|
||||
done <<< "$pkg_dateien"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Auftragsbuch: einen Vorschlags-Branch ANNEHMEN — der letzte Meter des propose-only-Kreislaufs.
|
||||
# Werkstatt/Orchestrator liefern Branches (wartung/*, orchestrator/*), der Commander klickt in
|
||||
# der Zentrale „Annehmen" → dieses Skript macht die bisherige Git-Handarbeit: Merge im
|
||||
# isolierten Worktree (Lehre: NIE im Live-Checkout arbeiten) → Push nach main → Deploy →
|
||||
# Health-Check → bei Rot automatischer Revert + Redeploy + Alarm.
|
||||
#
|
||||
# WICHTIG: läuft als EIGENE systemd-Unit (systemd-run, startet routers/auftragsbuch.py),
|
||||
# NICHT als Kind des Backends — deploy.sh startet mission-control-2 neu und würde sonst
|
||||
# den eigenen Eltern-Prozess mitten im Lauf töten. Außerdem wird es vom Backend als
|
||||
# /tmp-KOPIE gestartet (deploy.sh macht git reset --hard → die Datei unter den Füßen
|
||||
# eines laufenden bash zu tauschen korrumpiert das Skript, bekannte Falle).
|
||||
#
|
||||
# Nutzung: auftrag-annehmen.sh <branch> (z. B. wartung/saubere-zusammenfassung)
|
||||
set -uo pipefail
|
||||
|
||||
BRANCH="${1:?Nutzung: auftrag-annehmen.sh <branch>}"
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
|
||||
API="${MC_API:-http://127.0.0.1:9001}"
|
||||
SLUG="$(echo "$BRANCH" | tr '/' '-')"
|
||||
WT="/tmp/annahme-$SLUG"
|
||||
LOG="/tmp/annahme-$SLUG.log"
|
||||
|
||||
notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; }
|
||||
|
||||
# Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur).
|
||||
status(){ # $1=state $2=detail
|
||||
python3 - "$STATUS" "$BRANCH" "$1" "$2" <<'PY'
|
||||
import json, os, sys, time
|
||||
p, branch, state, detail = sys.argv[1:5]
|
||||
try:
|
||||
d = json.load(open(p, encoding="utf-8"))
|
||||
except Exception:
|
||||
d = {}
|
||||
d.setdefault("branches", {})[branch] = {"state": state, "detail": detail, "ts": time.time()}
|
||||
tmp = p + ".tmp"
|
||||
json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
os.replace(tmp, p)
|
||||
PY
|
||||
}
|
||||
|
||||
cleanup_wt(){ git -C "$SRC" worktree remove --force "$WT" 2>/dev/null || true; }
|
||||
|
||||
fail(){
|
||||
status "fehlgeschlagen" "$1"
|
||||
notify "Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen."
|
||||
cleanup_wt
|
||||
exit 1
|
||||
}
|
||||
|
||||
status "laeuft" "Merge wird vorbereitet"
|
||||
cd "$SRC" || fail "Live-Checkout $SRC fehlt"
|
||||
git fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen"
|
||||
git rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \
|
||||
|| fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)"
|
||||
|
||||
PRE="$(git rev-parse origin/main)"
|
||||
|
||||
# Kaputt aufgesetzte Branches (Worker machte git init/Shallow statt zu klonen) haben KEINEN
|
||||
# gemeinsamen Vorfahren — git verweigert den Merge immer. Ehrlich sagen statt „Konflikt".
|
||||
git merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \
|
||||
|| fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben"
|
||||
|
||||
# Isolierter Worktree — der Live-Checkout bleibt bis zum Deploy unberührt.
|
||||
cleanup_wt
|
||||
git worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden"
|
||||
MERGE_MSG="Auftragsbuch: '$BRANCH' angenommen (Ein-Klick-Gate)"
|
||||
if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then
|
||||
git -C "$WT" merge --abort 2>/dev/null || true
|
||||
# Selbstheilung: veralteten Branch mechanisch auf main rebasen (zweiter Worktree am
|
||||
# Branch-Kopf). Klappt das sauber, wird der rebasede Stand gemergt — kein PC nötig.
|
||||
status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht"
|
||||
WT2="/tmp/annahme-rebase-$SLUG"
|
||||
git worktree remove --force "$WT2" 2>/dev/null || true
|
||||
REBASED=""
|
||||
if git worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \
|
||||
&& git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
rebase origin/main >>"$LOG" 2>&1; then
|
||||
REBASED="$(git -C "$WT2" rev-parse HEAD)"
|
||||
else
|
||||
git -C "$WT2" rebase --abort 2>/dev/null || true
|
||||
fi
|
||||
git worktree remove --force "$WT2" 2>/dev/null || true
|
||||
[ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)"
|
||||
git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \
|
||||
|| { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; }
|
||||
fi
|
||||
|
||||
# py_compile-Gate über die durch den Merge geänderten Python-Dateien (AGENTS.md-Regel).
|
||||
PYS="$(git -C "$WT" diff --name-only "$PRE"..HEAD -- '*.py' 2>/dev/null | tr '\n' ' ')"
|
||||
if [ -n "${PYS// /}" ]; then
|
||||
PYBIN="$SRC/backend/.venv/bin/python"; [ -x "$PYBIN" ] || PYBIN="python3"
|
||||
# shellcheck disable=SC2086
|
||||
( cd "$WT" && "$PYBIN" -m py_compile $PYS ) >>"$LOG" 2>&1 || fail "py_compile-Gate rot — der Patch enthält kaputtes Python"
|
||||
fi
|
||||
|
||||
status "laeuft" "Push nach main läuft"
|
||||
PUSHED=0
|
||||
for _ in 1 2 3; do
|
||||
git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; }
|
||||
sleep 5
|
||||
done
|
||||
[ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)"
|
||||
cleanup_wt
|
||||
|
||||
status "laeuft" "Deploy läuft — die Zentrale startet gleich kurz neu"
|
||||
notify "Vorschlag '$BRANCH' angenommen: Merge auf main ist durch, Deploy läuft. Die Zentrale ist gleich kurz weg."
|
||||
|
||||
# deploy.sh als Kopie ausführen (Selbst-Reset-Falle: es resettet das Repo, in dem es liegt).
|
||||
cp "$SRC/deploy/deploy.sh" "/tmp/annahme-deploy-$SLUG.sh"
|
||||
DEPLOY_OK=1
|
||||
bash "/tmp/annahme-deploy-$SLUG.sh" >>"$LOG" 2>&1 || DEPLOY_OK=0
|
||||
|
||||
# Health-Check mit Geduld (Dienst-Neustart + Warmup brauchen einen Moment).
|
||||
HEALTH=0
|
||||
if [ "$DEPLOY_OK" = 1 ]; then
|
||||
for _ in $(seq 1 18); do
|
||||
curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { HEALTH=1; break; }
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$HEALTH" = 1 ]; then
|
||||
# Gemergt + live + grün → der Remote-Branch hat seinen Zweck erfüllt.
|
||||
git -C "$SRC" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true
|
||||
git -C "$SRC" fetch -q --prune origin 2>/dev/null || true
|
||||
status "eingespielt" "Deploy grün, Health-Check bestanden"
|
||||
notify "Vorschlag '$BRANCH' ist LIVE — Deploy grün, Health-Check bestanden. ✅"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Rollback: Merge revertieren, main zurückschieben, neu deployen, Alarm ──
|
||||
status "rollback" "Health rot — automatischer Revert läuft"
|
||||
git fetch -q origin || true
|
||||
cleanup_wt
|
||||
if git worktree add --detach "$WT" origin/main >/dev/null 2>&1 \
|
||||
&& git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \
|
||||
&& git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then
|
||||
cleanup_wt
|
||||
cp "$SRC/deploy/deploy.sh" "/tmp/annahme-rollback-$SLUG.sh"
|
||||
bash "/tmp/annahme-rollback-$SLUG.sh" >>"$LOG" 2>&1 || true
|
||||
R=0
|
||||
for _ in $(seq 1 12); do
|
||||
curl -sf -m 5 "$API/api/health" >/dev/null 2>&1 && { R=1; break; }
|
||||
sleep 5
|
||||
done
|
||||
if [ "$R" = 1 ]; then
|
||||
status "zurueckgerollt" "Health blieb rot — Merge automatisch revertiert, Box läuft wieder"
|
||||
notify "Vorschlag '$BRANCH' hat den Health-Check GERISSEN. Automatisch zurückgerollt — die Box läuft wieder auf dem alten Stand. Der Branch bleibt zur Analyse liegen. Log: $LOG"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cleanup_wt
|
||||
status "kritisch" "Rollback fehlgeschlagen — Box braucht Hilfe"
|
||||
curl -sf -m 5 -X POST "$API/api/alarm" -H 'Content-Type: application/json' \
|
||||
--data "{\"subject\":\"[Auftragsbuch]\",\"text\":\"KRITISCH: Annehmen von $BRANCH UND Rollback fehlgeschlagen — Zentrale prüfen. Log: $LOG\"}" >/dev/null 2>&1 \
|
||||
|| notify "KRITISCH: Annehmen von '$BRANCH' UND Rollback fehlgeschlagen — bitte Box prüfen (restore.sh liegt bereit). Log: $LOG"
|
||||
exit 2
|
||||
+62
-1
@@ -123,7 +123,7 @@ check_hermes(){
|
||||
done
|
||||
if [ "$state" = "done" ]; then
|
||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Gedächtnis, Tools, Voice) grün — alles läuft."
|
||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Patches, Tools, Voice) grün — alles läuft."
|
||||
return
|
||||
fi
|
||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
||||
@@ -165,6 +165,63 @@ Wahrscheinliche Ursache/Fix (Box-Diagnose): $sugg}"
|
||||
notify "KRITISCH: Hermes-Update UND Rollback fehlgeschlagen — Gehirn-Check bleibt rot. Bitte melden. (Voll-Restore: deploy/restore.sh mit dem letzten Backup.)"
|
||||
}
|
||||
|
||||
# ── Neustart, wenn das OS einen verlangt ─────────────────────────────────────
|
||||
# Kernel- und libc-Updates werden erst nach einem Neustart wirksam; bis dahin
|
||||
# laeuft die Box weiter auf dem alten Kernel. Das faellt niemandem auf — am
|
||||
# 27.08.2026 lag die Box 7 Wochen mit drei ungenutzten Kerneln da.
|
||||
#
|
||||
# Der Neustart haengt bewusst am WOECHENTLICHEN Lauf (So 04:30) und nicht an
|
||||
# einem eigenen Timer: ein Ort fuer alle Automatik (Lehre vom 20.08.).
|
||||
# Er passiert NUR, wenn Ubuntu ihn selbst anfordert (/var/run/reboot-required).
|
||||
#
|
||||
# WICHTIG: Die Linger-Pruefung ist die entscheidende Zeile hier. Ohne Linger=yes
|
||||
# startet systemd die User-Dienste nach einem Neustart NICHT — die Box kaeme ohne
|
||||
# Steuerpult, Gateway und Waechter hoch, und niemand koennte sie aus der Ferne
|
||||
# wieder anschalten. Im Zweifel lieber nicht neustarten.
|
||||
#
|
||||
# Abschalten: MC_AUTOUPDATE_REBOOT=0 in der Umgebung des Cron-Jobs.
|
||||
reboot_wenn_noetig(){
|
||||
[ "${MC_AUTOUPDATE_REBOOT:-1}" = "1" ] || { say "Neustart per Env abgeschaltet."; return 0; }
|
||||
[ -f /var/run/reboot-required ] || { say "Kein Neustart noetig."; return 0; }
|
||||
|
||||
local pakete; pakete="$(sort -u /var/run/reboot-required.pkgs 2>/dev/null | tr "
|
||||
" " ")"
|
||||
|
||||
# Sicherung 1: kommen die Dienste ohne Login von selbst wieder hoch?
|
||||
# $USER ist in systemd-/Cron-Umgebungen NICHT gesetzt — mit set -u waere das ein
|
||||
# sofortiger Abbruch des ganzen Update-Laufs. id -un funktioniert immer.
|
||||
local nutzer; nutzer="$(id -un)"
|
||||
if [ "$(loginctl show-user "$nutzer" -p Linger --value 2>/dev/null)" != "yes" ]; then
|
||||
notify "Neustart NICHT ausgefuehrt: die Box laeuft ohne linger — nach einem Neustart wuerden Steuerpult, Gateway und Waechter nicht von selbst starten. Erst 'loginctl enable-linger $nutzer' setzen, dann von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sicherung 2: kein halbfertiges Update mitten im Neustart abwuergen.
|
||||
local laufend
|
||||
laufend="$(curl -sf --max-time 20 "$API/api/jobs" | jq -r '[.jobs[]? | select(.state=="running")] | length' 2>/dev/null || echo 0)"
|
||||
if [ "${laufend:-0}" -gt 0 ]; then
|
||||
notify "Neustart verschoben: es laufen noch $laufend Job(s). Ausstehend: ${pakete:-Kernel/libc}. Naechster Versuch beim Lauf in einer Woche."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Sicherung 3: die Dienste muessen fuer den Autostart vorgemerkt sein.
|
||||
local fehlend=""
|
||||
for dienst in mission-control-2 mc2-gateway mc2-steward; do
|
||||
systemctl --user is-enabled "$dienst" >/dev/null 2>&1 || fehlend="$fehlend $dienst"
|
||||
done
|
||||
if [ -n "$fehlend" ]; then
|
||||
notify "Neustart NICHT ausgefuehrt: diese Dienste sind nicht fuer den Autostart eingetragen —$fehlend. Erst 'systemctl --user enable' nachholen. Ausstehend: ${pakete:-Kernel/libc}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
notify "Die Box startet jetzt neu — das OS verlangt es nach dem Update (${pakete:-Kernel/libc}). Sie ist ein paar Minuten weg und meldet sich, sobald alles wieder laeuft. Die Modelle muessen danach neu geladen werden, die erste Anfrage dauert also laenger."
|
||||
say "Neustart wird ausgeloest (ausstehend: ${pakete:-Kernel/libc})."
|
||||
sleep 20 # der Meldung Zeit lassen, rauszugehen
|
||||
sudo -n systemctl reboot || {
|
||||
notify "Neustart FEHLGESCHLAGEN: 'sudo systemctl reboot' wurde abgelehnt. Bitte von Hand neustarten. Ausstehend: ${pakete:-Kernel/libc}"
|
||||
}
|
||||
}
|
||||
|
||||
# ── Lauf ─────────────────────────────────────────────────────────────────────
|
||||
say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
|
||||
pins_init
|
||||
@@ -180,3 +237,7 @@ check_hermes
|
||||
notify "Commander, die Wochenpflege der Box ist durch — kurz für dich:
|
||||
$(printf '• %s\n' "${SUMMARY[@]}")"
|
||||
say "Fertig."
|
||||
|
||||
# Ganz zuletzt: der Neustart wuerde die Abschlussmeldung oben sonst verschlucken.
|
||||
# Die Funktion meldet in jedem Fall selbst, ob sie neustartet oder warum nicht.
|
||||
reboot_wenn_noetig
|
||||
|
||||
+3
-1
@@ -5,6 +5,8 @@
|
||||
#
|
||||
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||
# known-good/ (Versions-Manifest, s. u.)
|
||||
# Das abgeloeste Gedaechtnis-Sidecar (/srv/models, Port 8765) faellt seit 27.08.2026 weg —
|
||||
# sein Datenverzeichnis existiert nicht mehr (Hintergrund: docs/wissen/VERDIKTE.md).
|
||||
# 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.
|
||||
@@ -58,7 +60,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||
|
||||
# pip freeze je venv; voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||
# pip freeze je venv; das voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bagatell-Annahme — „Harmloses darf selbst" (User-Entscheid 10.07.2026, konservativ geschnitten):
|
||||
# Vorschlags-Branches, die AUSSCHLIESSLICH Markdown-Dateien anlegen oder ändern (Doku/Wiki —
|
||||
# keine einzige Code-Zeile, keine Löschungen, keine Umbenennungen, keine Skripte), spielt die
|
||||
# Box nachts OHNE Klick ein. Der Weg ist derselbe wie beim Klick im Auftragsbuch:
|
||||
# auftrag-annehmen.sh (Merge im Worktree → py_compile-Gate → Push main → Deploy → Health →
|
||||
# Auto-Revert). Fangnetz drumherum: Nacht-Sicherung 03:30 (Zeitmaschine) liegt davor, die
|
||||
# Morgenlage 04:30 berichtet danach, jede Annahme steht in Chronik + Telegram.
|
||||
# JEDE Code-Zeile bleibt Klick-pflichtig — dieses Skript weitet die Klasse NIE selbst aus.
|
||||
#
|
||||
# Läuft als mc2-bagatell.timer (04:10) über eine /tmp-Kopie (Selbst-Reset-Falle: die Annahme
|
||||
# deployt und resettet damit das Repo, in dem dieses Skript liegt).
|
||||
set -uo pipefail
|
||||
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
|
||||
MAX_PRO_NACHT=2 # jede Annahme startet die Zentrale kurz neu — Nachtruhe wahren
|
||||
|
||||
cd "$SRC" || exit 0
|
||||
git fetch -q --prune origin 2>/dev/null || exit 0
|
||||
|
||||
angenommen=0
|
||||
for REF in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin); do
|
||||
[ "$angenommen" -ge "$MAX_PRO_NACHT" ] && break
|
||||
BRANCH="${REF#origin/}"
|
||||
case "$BRANCH" in wartung/*|orchestrator/*|doku/*|feature/*) ;; *) continue ;; esac
|
||||
|
||||
AHEAD="$(git rev-list --count "origin/main..$REF" 2>/dev/null || echo 0)"
|
||||
[ "${AHEAD:-0}" -gt 0 ] || continue
|
||||
|
||||
# Nur unberührte Vorschläge: trägt der Branch schon irgendeinen Annahme-Status
|
||||
# (läuft/fehlgeschlagen/zurückgerollt/…), entscheidet der Commander — nicht wir.
|
||||
STATE="$(python3 - "$STATUS" "$BRANCH" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
print((json.load(open(sys.argv[1], encoding="utf-8")).get("branches", {}).get(sys.argv[2]) or {}).get("state", ""))
|
||||
except Exception:
|
||||
print("")
|
||||
PY
|
||||
)"
|
||||
[ -z "$STATE" ] || continue
|
||||
|
||||
# Bagatell-Klasse: JEDE geänderte Datei endet auf .md UND ist nur angelegt (A) oder
|
||||
# geändert (M). Alles andere (Löschung, Rename R…, Copy C…, Nicht-Markdown) → Klick-Pflicht.
|
||||
DIFF="$(git diff --name-status "origin/main...$REF" 2>/dev/null)"
|
||||
[ -n "$DIFF" ] || continue
|
||||
OK=1
|
||||
while IFS=$'\t' read -r st path _rest; do
|
||||
[ -n "$st" ] || continue
|
||||
case "$st" in A|M) ;; *) OK=0; break ;; esac
|
||||
case "$path" in *.md) ;; *) OK=0; break ;; esac
|
||||
done <<< "$DIFF"
|
||||
[ "$OK" = 1 ] || continue
|
||||
|
||||
N="$(printf '%s\n' "$DIFF" | wc -l | tr -d ' ')"
|
||||
echo "Bagatelle erkannt: $BRANCH ($N Markdown-Datei(en))"
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(jq -n --arg t "Bagatelle (nur Doku, $N Datei(en)): '$BRANCH' wird ohne Klick eingespielt — Fangnetz aktiv, Details in der Morgenlage." \
|
||||
'{text:$t, subject:"[Bagatell-Annahme]", source:"bagatell", priority:"silent"}')" >/dev/null 2>&1 || true
|
||||
|
||||
RUNNER="/tmp/mc2-bagatell-annehmen-$$.sh"
|
||||
cp "$SRC/deploy/auftrag-annehmen.sh" "$RUNNER" || continue
|
||||
if bash "$RUNNER" "$BRANCH"; then
|
||||
angenommen=$((angenommen+1))
|
||||
else
|
||||
# auftrag-annehmen.sh hat selbst gemeldet (fehlgeschlagen/zurückgerollt) und aufgeräumt;
|
||||
# der Branch trägt jetzt einen Status → nächste Nacht fasst ihn niemand mehr automatisch an.
|
||||
echo "Bagatell-Annahme von $BRANCH nicht grün — Meldung kam vom Annahme-Runner."
|
||||
fi
|
||||
done
|
||||
echo "Bagatell-Lauf fertig: $angenommen eingespielt."
|
||||
@@ -7,7 +7,7 @@ set -u
|
||||
MODEL="${BRAIN_GGUF:-/srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf}"
|
||||
BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
|
||||
PORT="${BENCH_PORT:-5899}"
|
||||
BASE="-c 65536 -ngl 999 -fa on --no-mmap --jinja --parallel 1"
|
||||
BASE="-c 65536 -ngl 999 -fa on --load-mode none --jinja --parallel 1"
|
||||
|
||||
run() { # $1=name $2=extra-flags
|
||||
echo "=== $1 ==="
|
||||
|
||||
@@ -13,7 +13,7 @@ PORT="${BENCH_PORT:-5899}"
|
||||
CTX="${BENCH_CTX:-32768}"
|
||||
|
||||
echo "=== Bench: $(basename "$MODEL") (ctx $CTX${EXTRA:+, $EXTRA}) ==="
|
||||
$BIN -m "$MODEL" --host 127.0.0.1 --port "$PORT" -c "$CTX" -ngl 999 -fa on --no-mmap --jinja $EXTRA \
|
||||
$BIN -m "$MODEL" --host 127.0.0.1 --port "$PORT" -c "$CTX" -ngl 999 -fa on --load-mode none --jinja $EXTRA \
|
||||
>/tmp/model-bench-server.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID 2>/dev/null; wait $PID 2>/dev/null' EXIT
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prüfstand für Hirn-Kandidaten (17.09.2026). Kandidat läuft standalone auf :5899 — der Live-Stack bleibt unberührt —,
|
||||
Baselines laufen über llama-swap :8080. Je Betriebsart: Kurz- und 13k-Tiefenmessung (Prefill/Decode/Draft-Akzeptanz),
|
||||
Tool-Calling direkt (3 Aufgaben, Denken an), Deutsch- und JSON-Probe (Denken aus) und der Tool-Smoke durch den ECHTEN
|
||||
Hermes-Agenten (`hermes chat --provider pruefstand -m kandidat`; Provider `pruefstand` -> http://127.0.0.1:5899/v1 muss in
|
||||
`providers:` der Hermes-Config stehen). Aufruf auf der Box: python3 pruefstand-hirn.py [baseline] [nemo] [u38] [u36]
|
||||
(Kandidaten-Pfade unten anpassen). Ergebnis: ~/pruefstand-<datum>.json + Log auf stdout. Lehren: Denk-Modelle brauchen
|
||||
für Tool-Aufrufe >=1500 Tokens Budget, /no_think wirkt bei Qwen 3.6 nicht (chat_template_kwargs.enable_thinking=false);
|
||||
der erste Lauf nach dem Laden ist ~40 % langsamer (Warmlauf verwerfen)."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
BIN = "/opt/llamacpp-vulkan/llama-server"; PORT = 5899
|
||||
BASE = ["-c", "65536", "-ngl", "999", "-fa", "on", "--load-mode", "none", "--jinja", "--parallel", "1", "-ctk", "q8_0", "-ctv", "q8_0"]
|
||||
M = "/srv/models"
|
||||
DFLASH_36 = f"{M}/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf"
|
||||
DFLASH2_38 = f"{M}/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf"
|
||||
NEMO = f"{M}/Nemotron-3.5-Lightning-30B-A3B-GGUF/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-Q4_0.gguf"
|
||||
NEMO_MTP = f"{M}/Nemotron-3.5-Lightning-30B-A3B-GGUF/mtp-NVIDIA-Nemotron-3.5-Lightning-30B-A3B-Q8_0.gguf"
|
||||
U38 = f"{M}/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf"
|
||||
U36 = f"{M}/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf"
|
||||
HERMES = os.path.expanduser("~/.local/bin/hermes")
|
||||
OUT = os.path.expanduser("~/pruefstand-2026-09-17.json")
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def log(*a):
|
||||
print(*a, flush=True)
|
||||
|
||||
|
||||
def post(url, payload, timeout=900):
|
||||
r = urllib.request.Request(url, data=json.dumps(payload).encode(), headers={"Content-Type": "application/json"})
|
||||
return json.loads(urllib.request.urlopen(r, timeout=timeout).read())
|
||||
|
||||
|
||||
def chat(base, model, messages, max_tokens=200, tools=None, think=True):
|
||||
p = {"model": model, "temperature": 0, "max_tokens": max_tokens, "messages": messages}
|
||||
if not think:
|
||||
p["chat_template_kwargs"] = {"enable_thinking": False}
|
||||
if tools:
|
||||
p["tools"] = tools
|
||||
p["tool_choice"] = "auto"
|
||||
t0 = time.time()
|
||||
j = post(f"{base}/v1/chat/completions", p)
|
||||
j["_wall"] = time.time() - t0
|
||||
return j
|
||||
|
||||
|
||||
PARA = ("Notiz {i}: Die Box läuft mit llama-swap auf Port 8080, das Hirn ist ein Mixture-of-Experts-Modell mit drei Milliarden "
|
||||
"aktiven Parametern, der Coder ein dichtes 27B-Modell mit Draft-Beschleunigung. Lucy hört über Parakeet, spricht über "
|
||||
"pocket-tts und meldet sich per Telegram. Der Sonntags-Job aktualisiert Router, Engine und Agent und rollt bei Rot zurück. ")
|
||||
DEPTH_PROMPT = "".join(PARA.format(i=i) for i in range(130)) + \
|
||||
"\n\nFrage: Fasse in drei Sätzen zusammen, was in den Notizen steht, und nenne die Nummer der letzten Notiz."
|
||||
|
||||
TOOLS = [
|
||||
{"type": "function", "function": {"name": "terminal", "description": "Führt einen Shell-Befehl auf der Box aus und gibt stdout zurück.",
|
||||
"parameters": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}}},
|
||||
{"type": "function", "function": {"name": "read_file", "description": "Liest eine Datei und gibt den Inhalt zurück.",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}},
|
||||
{"type": "function", "function": {"name": "send_telegram", "description": "Schickt dem Nutzer eine Telegram-Nachricht.",
|
||||
"parameters": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}},
|
||||
]
|
||||
TOOL_TASKS = [
|
||||
("Wie viel freier Speicher ist auf /srv/models? Nutze das passende Werkzeug.", "terminal", r"df"),
|
||||
("Lies die Datei /etc/hostname und sag mir, wie die Box heißt.", "read_file", r"/etc/hostname"),
|
||||
("Schick mir per Telegram die Nachricht: Prüfstand läuft.", "send_telegram", r"Pr.fstand"),
|
||||
]
|
||||
|
||||
|
||||
def timings(j):
|
||||
t = j.get("timings") or {}
|
||||
acc = (t.get("draft_n_accepted", 0) / t["draft_n"]) if t.get("draft_n") else None
|
||||
return {"pp_n": t.get("prompt_n"), "pp_s": round((t.get("prompt_ms") or 0) / 1000, 1), "pp_tps": round(t.get("prompt_per_second") or 0),
|
||||
"tg_n": t.get("predicted_n"), "tg_tps": round(t.get("predicted_per_second") or 0, 1), "acc": None if acc is None else round(acc, 2)}
|
||||
|
||||
|
||||
def save():
|
||||
json.dump(RESULTS, open(OUT, "w"), indent=1, ensure_ascii=False)
|
||||
|
||||
|
||||
def probes(base, model, label, with_hermes, hermes_args):
|
||||
res = {"label": label}
|
||||
chat(base, model, [{"role": "user", "content": "Sag kurz hallo."}], 32) # Warmlauf
|
||||
j = chat(base, model, [{"role": "user", "content": "Erkläre in fünf Sätzen, was ein Mixture-of-Experts-Modell ist."}], 200)
|
||||
res["kurz"] = timings(j); log(f" kurz : {res['kurz']}")
|
||||
j = chat(base, model, [{"role": "user", "content": DEPTH_PROMPT}], 160)
|
||||
res["tiefe"] = timings(j); log(f" 13k : {res['tiefe']}")
|
||||
tc = []
|
||||
for q, want_fn, want_arg in TOOL_TASKS:
|
||||
try:
|
||||
j = chat(base, model, [{"role": "user", "content": q}], 1500, TOOLS)
|
||||
msg = j["choices"][0]["message"]
|
||||
calls = msg.get("tool_calls") or []
|
||||
ok = any(c["function"]["name"] == want_fn and re.search(want_arg, c["function"].get("arguments", "")) for c in calls)
|
||||
raw = "" if calls else (msg.get("content") or "")[:120].replace("\n", " ")
|
||||
tc.append({"task": want_fn, "ok": ok, "calls": [(c["function"]["name"], c["function"].get("arguments", "")[:80]) for c in calls], "raw": raw})
|
||||
except Exception as e:
|
||||
tc.append({"task": want_fn, "ok": False, "error": str(e)[:120]})
|
||||
res["tools_direkt"] = tc
|
||||
log(f" tools: {sum(1 for t in tc if t['ok'])}/3 " + "; ".join(f"{t['task']}={'✓' if t['ok'] else '✗'}" for t in tc))
|
||||
for t in tc:
|
||||
if not t["ok"]:
|
||||
log(f" {t['task']}: calls={t.get('calls')} raw={t.get('raw', '')!r} {t.get('error', '')}")
|
||||
j = chat(base, model, [{"role": "user", "content": "Antworte auf Deutsch in genau drei Sätzen: Warum braucht ein Heimserver ein Backup?"}], 300, think=False)
|
||||
m = j["choices"][0]["message"]
|
||||
txt = (m.get("content") or "").strip() or ("[nur reasoning] " + (m.get("reasoning_content") or "")[:200])
|
||||
res["deutsch"] = txt[:400]; log(f" de : {txt[:160]!r}")
|
||||
j = chat(base, model, [{"role": "user", "content": 'Antworte NUR mit JSON, ohne Erklärung: {"modell": "<dein Name>", "ok": true}'}], 120, think=False)
|
||||
m = j["choices"][0]["message"]
|
||||
txt = (m.get("content") or "").strip()
|
||||
try:
|
||||
json.loads(txt[txt.find("{"): txt.rfind("}") + 1]); js = True
|
||||
except Exception:
|
||||
js = False
|
||||
res["json_ok"] = js; log(f" json : {'✓' if js else '✗'} {txt[:80]!r}")
|
||||
if with_hermes:
|
||||
hz = []
|
||||
for q, want in [("Führe im Terminal exakt den Befehl echo pruefstand-ok aus und gib mir nur dessen Ausgabe zurück.", "pruefstand-ok"),
|
||||
("Welche Verzeichnisse liegen direkt unter /srv/models? Nenne mir drei davon.", "GGUF")]:
|
||||
t0 = time.time()
|
||||
try:
|
||||
p = subprocess.run([HERMES, "chat", *hermes_args, "-Q", "--oneshot", "--max-turns", "6", "-q", q],
|
||||
capture_output=True, text=True, timeout=900)
|
||||
out = (p.stdout + p.stderr)[-600:]
|
||||
except subprocess.TimeoutExpired:
|
||||
out = "TIMEOUT"
|
||||
ok = want.lower() in out.lower()
|
||||
hz.append({"ok": ok, "s": round(time.time() - t0), "tail": out[-160:].replace("\n", " ")})
|
||||
log(f" hermes: {'✓' if ok else '✗'} {round(time.time() - t0)} s {out[-120:].replace(chr(10), ' ')!r}")
|
||||
res["hermes"] = hz
|
||||
RESULTS.append(res); save()
|
||||
|
||||
|
||||
def start_server(model, extra):
|
||||
cmd = [BIN, "-m", model, "--host", "127.0.0.1", "--port", str(PORT), *BASE, *extra]
|
||||
logf = open("/tmp/pruefstand-server.log", "w")
|
||||
p = subprocess.Popen(cmd, stdout=logf, stderr=subprocess.STDOUT)
|
||||
t0 = time.time()
|
||||
for _ in range(150):
|
||||
time.sleep(2)
|
||||
if p.poll() is not None:
|
||||
tail = open("/tmp/pruefstand-server.log", errors="replace").read()[-500:]
|
||||
return None, f"LOAD-CRASH rc={p.returncode}: {tail.strip()[-300:]}"
|
||||
try:
|
||||
if b"ok" in urllib.request.urlopen(f"http://127.0.0.1:{PORT}/health", timeout=2).read():
|
||||
return p, round(time.time() - t0)
|
||||
except Exception:
|
||||
pass
|
||||
p.kill()
|
||||
return None, "TIMEOUT beim Laden"
|
||||
|
||||
|
||||
def candidate(label, model, extra, with_hermes):
|
||||
log(f"\n=== {label}"); log(f" {os.path.basename(model)} {' '.join(extra)}")
|
||||
if not os.path.exists(model):
|
||||
log(" FEHLT: Datei nicht da"); RESULTS.append({"label": label, "fehler": "Datei fehlt"}); save(); return
|
||||
p, info = start_server(model, extra)
|
||||
if p is None:
|
||||
log(f" {info}"); RESULTS.append({"label": label, "fehler": info}); save(); return
|
||||
log(f" geladen in {info} s")
|
||||
try:
|
||||
probes(f"http://127.0.0.1:{PORT}", "kandidat", label, with_hermes, ["--provider", "pruefstand", "-m", "kandidat"])
|
||||
except Exception as e:
|
||||
log(f" FEHLER in Proben: {e}"); RESULTS.append({"label": label, "fehler": str(e)[:200]}); save()
|
||||
finally:
|
||||
p.terminate()
|
||||
try:
|
||||
p.wait(timeout=60)
|
||||
except Exception:
|
||||
p.kill()
|
||||
time.sleep(3)
|
||||
|
||||
|
||||
which = set(sys.argv[1:]) or {"baseline", "nemo", "u38", "u36"}
|
||||
if "baseline" in which:
|
||||
log("\n=== BASELINE fast (live, llama-swap)"); probes("http://127.0.0.1:8080", "fast", "fast-live", True, ["-m", "fast"])
|
||||
log("\n=== BASELINE coder = Qwen3.8-27B+DFlash2 (live)"); probes("http://127.0.0.1:8080", "coder", "coder-live", True, ["-m", "coder"])
|
||||
if "nemo" in which:
|
||||
candidate("Nemotron 3.5 Lightning · MTP n3", NEMO, ["--spec-type", "draft-mtp", "--spec-draft-model", NEMO_MTP, "--spec-draft-n-max", "3"], True)
|
||||
candidate("Nemotron 3.5 Lightning · ohne Spec", NEMO, [], False)
|
||||
if "u38" in which:
|
||||
candidate("Qwen3.8-27B-Uncensored · MTP fused n2", U38, ["--spec-type", "draft-mtp", "--spec-draft-n-max", "2"], True)
|
||||
candidate("Qwen3.8-27B-Uncensored · DFlash2 (z-lab) n3", U38, ["--spec-type", "draft-dflash", "--spec-draft-model", DFLASH2_38, "--spec-draft-n-max", "3"], False)
|
||||
candidate("Qwen3.8-27B-Uncensored · ohne Spec", U38, [], False)
|
||||
if "u36" in which:
|
||||
candidate("Qwen3.6-35B-A3B-Uncensored-Aggressive · DFlash (giocom)", U36, ["--spec-type", "draft-dflash", "--spec-draft-model", DFLASH_36], True)
|
||||
candidate("Qwen3.6-35B-A3B-Uncensored-Aggressive · ohne Spec", U36, [], False)
|
||||
log("\nPRUEFSTAND_DONE")
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/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"
|
||||
+66
-31
@@ -1,41 +1,76 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Starte MC2 Deployment..."
|
||||
# ‼️ Das ganze Skript liegt in EINER Funktion — das ist Absicht, kein Stil.
|
||||
# Bash liest ein Skript haeppchenweise WAEHREND es laeuft. `git pull` unten ersetzt
|
||||
# genau diese Datei mitten im Lauf; danach liest bash am selben Byte-Offset im NEUEN
|
||||
# Text weiter und fuehrt zerschnittene Zeilen aus. Live erlebt am 27.08.2026: der Pull
|
||||
# brachte eine neue Fassung dieses Schrittes mit, ausgefuehrt wurde trotzdem die alte.
|
||||
# Eine Funktion wird komplett geparst, bevor sie startet -> der Lauf bleibt in sich
|
||||
# stimmig, und Aenderungen greifen ab dem naechsten Aufruf statt mittendrin.
|
||||
main() {
|
||||
|
||||
# 1. Neuesten Code holen
|
||||
git pull origin main
|
||||
echo "Starte MC2 Deployment..."
|
||||
|
||||
# 1. Neuesten Code holen
|
||||
git pull origin main
|
||||
|
||||
|
||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||
echo "Aktualisiere Python Abhängigkeiten..."
|
||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||
echo "Aktualisiere Python Abhängigkeiten..."
|
||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||
|
||||
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
||||
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
|
||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
||||
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||
sudo systemctl restart llama-swap
|
||||
fi
|
||||
|
||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||
systemctl --user daemon-reload
|
||||
|
||||
# 4.5 Hermes Skills synchronisieren
|
||||
echo "Synchronisiere Hermes Skills..."
|
||||
mkdir -p ~/.hermes/skills
|
||||
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
|
||||
for skill_dir in deploy/skills/*; do
|
||||
if [ -d "$skill_dir" ]; then
|
||||
skill_name=$(basename "$skill_dir" | tr '-' '_')
|
||||
mkdir -p ~/.hermes/skills/"$skill_name"
|
||||
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
|
||||
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
||||
#
|
||||
# ‼️ ACHTUNG (27.08.2026): /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit — MC2
|
||||
# schreibt sie selbst (backend/services/llamaswap.py::write_config), sobald jemand über
|
||||
# die Oberfläche ein Modell hinzufügt oder eine Rolle ändert. deploy/llama-swap.config.yaml
|
||||
# ist nur ein Abzug und hinkt regelmäßig hinterher (erlebt: der Coder-Vollkontext aus 34a9862
|
||||
# stand tagelang nur live, nicht im Abzug — ein Deploy hätte ihn zurückgesetzt).
|
||||
# Bis hierher wurde die Live-Datei kommentarlos mit dem Abzug überschrieben — alles per
|
||||
# UI Hinzugefügte war nach jedem Deploy weg. Jetzt: erst sichern, dann kopieren, und die
|
||||
# Abweichung sichtbar machen. MC_DEPLOY_SKIP_SWAP_CONFIG=1 überspringt den Schritt ganz.
|
||||
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" = "1" ]; then
|
||||
echo "Llama-Swap Konfiguration weicht ab — uebersprungen (MC_DEPLOY_SKIP_SWAP_CONFIG=1)."
|
||||
else
|
||||
SWAP_BAK="/etc/llama-swap/config.yaml.bak-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "Llama-Swap Konfiguration hat sich geändert."
|
||||
echo " Abweichung (live -> Repo-Abzug):"
|
||||
diff -u /etc/llama-swap/config.yaml deploy/llama-swap.config.yaml | head -40 || true
|
||||
echo " Sichere die laufende Datei nach $SWAP_BAK"
|
||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
||||
sudo cp /etc/llama-swap/config.yaml "$SWAP_BAK"
|
||||
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||
sudo systemctl restart llama-swap
|
||||
echo " Rueckweg bei Verlust: sudo cp $SWAP_BAK /etc/llama-swap/config.yaml"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# 5. Dienste neu starten
|
||||
echo "Starte Backend neu..."
|
||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||
systemctl --user daemon-reload
|
||||
|
||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
||||
# 4.5 Hermes Skills synchronisieren
|
||||
echo "Synchronisiere Hermes Skills..."
|
||||
mkdir -p ~/.hermes/skills
|
||||
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
|
||||
for skill_dir in deploy/skills/*; do
|
||||
if [ -d "$skill_dir" ]; then
|
||||
skill_name=$(basename "$skill_dir" | tr '-' '_')
|
||||
mkdir -p ~/.hermes/skills/"$skill_name"
|
||||
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
|
||||
fi
|
||||
done
|
||||
|
||||
# 5. Dienste neu starten
|
||||
# ‼️ mc2-steward gehoert hier dazu: er laeuft aus DEMSELBEN Checkout (backend/steward.py).
|
||||
# Ohne ihn lief der Waechter-Prozess nach jedem Deploy mit dem alten Code weiter — der
|
||||
# Neustart wurde 27.08.2026 nachgetragen. `--user restart` auf einer nicht installierten
|
||||
# Unit ist folgenlos, darum ohne Vorabpruefung.
|
||||
echo "Starte Backend neu..."
|
||||
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service
|
||||
|
||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
+15
-19
@@ -1,7 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
||||
# (die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
||||
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
||||
# Post-Update-Check nach einem Hermes-Agent-Update: laeuft unser "Gehirn" noch?
|
||||
# Exit 0 = alles ok, sonst 1 -> der Job wird im UI rot, damit ein kaputtes Gehirn auffaellt.
|
||||
#
|
||||
# WICHTIG (27.08.2026) - VIER TOTE CHECKS ENTFERNT: bis hierher prueften die ersten Bloecke
|
||||
# den abgeloesten Gedaechtnis-Sidecar (:8765), MC2s /api/memory und das zugehoerige
|
||||
# Hermes-Plugin. Alle drei fielen am 07.08. weg (docs/wissen/VERDIKTE.md) - die Checks KONNTEN nicht
|
||||
# mehr gruen werden. Folge: jedes Hermes-Update endete rot und wurde von autoupdate.sh
|
||||
# zurueckgerollt. Kein Ersatz-Check auf Hermes native Gedaechtnis, weil es dafuer keinen
|
||||
# nachgemessenen Endpunkt gibt - geraten waere eine Fassade. Der Tool-Smoke weiter unten
|
||||
# laeuft ohnehin durch den ECHTEN Agenten und faengt ein kaputtes Gehirn.
|
||||
set -uo pipefail
|
||||
|
||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||
@@ -10,13 +17,6 @@ fail=0
|
||||
|
||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
||||
|
||||
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
||||
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
||||
echo "PASS · memory.provider=mc2-memory aktiv"
|
||||
else
|
||||
echo "FAIL · memory.provider nicht mehr gesetzt (Config vom Update überschrieben?)"; fail=1
|
||||
fi
|
||||
|
||||
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
||||
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
||||
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
||||
@@ -33,15 +33,11 @@ if [ -f "$PATCH_APPLY" ]; then
|
||||
echo "FAIL · Hermes-Runtime-Patches: ein Patch passt nicht mehr zum aktualisierten Quellcode — pruefen"; fail=1
|
||||
fi
|
||||
else
|
||||
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
||||
fi
|
||||
|
||||
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
||||
"import sys; sys.path.insert(0,'.'); from plugins.memory import load_memory_provider; p=load_memory_provider('mc2-memory'); assert p and p.name()=='mc2-memory'" \
|
||||
>/dev/null 2>&1 ); then
|
||||
echo "PASS · mc2-memory-Plugin lädt unter dem neuen Hermes"
|
||||
else
|
||||
echo "FAIL · mc2-memory-Plugin lädt nicht (MemoryProvider-ABC geändert?)"; fail=1
|
||||
# Kein Fehler: Der Patch-Traeger wurde mit dem MC2-Kahlschlag (1e68f62) entfernt. Seine
|
||||
# Fixes zielten auf einen Hermes-Stand, der inzwischen tausende Commits zurueckliegt —
|
||||
# sie wuerden auf dem heutigen Quellcode ohnehin nicht mehr greifen. WARN statt FAIL, damit
|
||||
# es sichtbar bleibt, falls jemand wieder Runtime-Patches braucht.
|
||||
echo "WARN · hermes-patches/apply.py nicht vorhanden (mit 1e68f62 bewusst entfernt) — uebersprungen"
|
||||
fi
|
||||
|
||||
# --- Config-Drift-Wächter (Lehre aus v0.18, 02.07.2026): Hermes fällt bei unbekannten ---
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/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)
|
||||
# Nach einem Neustart ist der "letzte Lauf" leer, obwohl nichts kaputt ist —
|
||||
# am 04.09. machte der Radar daraus "hohes Risiko, sofort in Betrieb nehmen".
|
||||
# Junge Box (< 1 Tag) + Timer hat einen naechsten Termin = wartet, kein Befund.
|
||||
naechster=$(systemctl --user show -p NextElapseUSecRealtime --value "${dienst%.service}.timer" 2>/dev/null | cut -d' ' -f2,3)
|
||||
if [ -z "$wann" ] && [ "$(cut -d. -f1 /proc/uptime)" -lt 86400 ] && [ -n "$naechster" ]; then
|
||||
ok "$dienst wartet auf den ersten Lauf seit dem Neustart (naechster: $naechster)"
|
||||
elif [ -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"
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/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
|
||||
|
||||
# Der Blick nach DRAUSSEN (04.09.2026): Releases, PRs, neue Entwurfsmodelle —
|
||||
# als Skript-Fakten, gegen den letzten Lauf verglichen. Ohne das lag DFlash2 fuer
|
||||
# den Coder zwei Wochen ungenutzt auf der Platte. Faellt es aus, bleibt der
|
||||
# Innen-Bericht vollstaendig; das Ausbleiben steht dann selbst als ACHTUNG drin.
|
||||
UPSTREAM_SKRIPT="${UPSTREAM_SKRIPT:-$HIER/stack-upstream.py}"
|
||||
if [ -f "$UPSTREAM_SKRIPT" ]; then
|
||||
UPSTREAM="$(timeout 240 python3 "$UPSTREAM_SKRIPT" 2>&1)" \
|
||||
|| UPSTREAM="UPSTREAM (Watchlist)
|
||||
ACHTUNG Upstream-Pruefung abgebrochen (exit $?) — $UPSTREAM_SKRIPT nachsehen"
|
||||
# Nur Aenderungen ans Modell. Beim ersten Lauf (04.09.) bekam es auch die
|
||||
# "unveraendert"-Zeilen und erfand zu jeder einen "naechsten Schritt" — der
|
||||
# Prompt-Hinweis half nicht. Fakten filtert das Skript, nicht die Prosa.
|
||||
ANZ_QUELLEN="$(printf '%s\n' "$UPSTREAM" | grep -cE '^\s+(OK|ACHTUNG)' || true)"
|
||||
UPSTREAM_NEU="$(printf '%s\n' "$UPSTREAM" | grep -A1 -E '^\s+ACHTUNG' | grep -vE '^--$' || true)"
|
||||
[ -z "$UPSTREAM_NEU" ] && UPSTREAM_NEU=" OK nichts Neues draussen ($ANZ_QUELLEN Quellen geprueft, alle unveraendert)"
|
||||
IST="$IST
|
||||
|
||||
UPSTREAM (was hat sich draussen getan?)
|
||||
$UPSTREAM_NEU"
|
||||
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.
|
||||
- Zeilen mit NEU im Abschnitt UPSTREAM sind Neuigkeiten von draussen. Nenne
|
||||
jede kurz und gib den Hinweis dahinter (der Pfeil-Satz) als naechsten Schritt
|
||||
wieder. 'erstmals erfasst' und 'unveraendert' sind KEINE Neuigkeiten.
|
||||
- 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
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""stack-upstream.py — der Blick nach draussen, in Fakten (04.09.2026).
|
||||
|
||||
Ergaenzt stack-ist.sh: das prueft, ob die Box STIMMT. Dieses Skript prueft, ob
|
||||
DRAUSSEN etwas passiert ist, das uns betrifft — neue Releases, gemergte PRs,
|
||||
neue Entwurfsmodelle, neue Modelle der Familien, die wir fahren.
|
||||
|
||||
Warum es das braucht: Am 19.08. lagen die DFlash2-Entwuerfe fuer den Coder auf
|
||||
der Platte, am 27.08. konnte llama.cpp sie, und bis zum 04.09. hat es niemand
|
||||
gemerkt — der Coder lief zwei Wochen mit 12,6 statt 31 t/s. Der KISS-Radar sah
|
||||
nur nach innen. Jetzt sieht er auch nach draussen, aber ohne Modell-Fantasie:
|
||||
jede Zeile hier ist ein abgerufener Wert, verglichen mit dem Wert vom letzten Lauf.
|
||||
|
||||
Quelle: deploy/trend-radar-watchlist.json (Eintraege mit `typ`, siehe unten).
|
||||
Zustand: ~/.hermes/state/stack-upstream.json (Wert je Eintrag vom letzten Lauf).
|
||||
Ausgabe: im Format von stack-ist.sh — `OK` / `ACHTUNG NEU:` / `ACHTUNG ... nicht abrufbar`.
|
||||
Beim ERSTEN Lauf wird nur erfasst (OK), nicht gemeldet — sonst waere jeder
|
||||
Eintrag sofort "neu".
|
||||
|
||||
Typen:
|
||||
github_issue repo, nummer → offen/geschlossen
|
||||
github_pr repo, nummer → offen/gemergt/geschlossen
|
||||
github_release repo → juengster Release-Tag
|
||||
github_release_major repo → nur Major-Sprung meldet
|
||||
github_branch repo, branch → juengster Commit (Datum)
|
||||
pypi paket → Version
|
||||
hf_author author → neue Modell-Repos des Autors
|
||||
url_zeilen url, muster, max_zeilen → Zeilen mit Muster, Aenderung = neu
|
||||
|
||||
Kein set -e-Aequivalent: jeder Eintrag wird einzeln behandelt, ein Fehler
|
||||
nimmt die anderen nicht mit.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
WATCHLIST = Path(sys.argv[1] if len(sys.argv) > 1 else
|
||||
os.path.expanduser("~/mission-control-v2/deploy/trend-radar-watchlist.json"))
|
||||
STATE = Path(os.environ.get("STACK_UPSTREAM_STATE",
|
||||
os.path.expanduser("~/.hermes/state/stack-upstream.json")))
|
||||
TIMEOUT = 20
|
||||
UA = "mc2-stack-upstream/1.0 (+https://github.com/Hitonabi)"
|
||||
|
||||
|
||||
def ok(msg: str) -> None:
|
||||
print(f" OK {msg}")
|
||||
|
||||
|
||||
def neu(msg: str) -> None:
|
||||
print(f" ACHTUNG NEU: {msg}")
|
||||
|
||||
|
||||
def warn(msg: str) -> None:
|
||||
print(f" ACHTUNG {msg}")
|
||||
|
||||
|
||||
def hole(url: str, json_erwartet: bool = True):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
|
||||
tok = os.environ.get("GITHUB_TOKEN")
|
||||
if tok and "api.github.com" in url:
|
||||
req.add_header("Authorization", f"Bearer {tok}")
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
roh = r.read().decode("utf-8", "replace")
|
||||
return json.loads(roh) if json_erwartet else roh
|
||||
|
||||
|
||||
# ------------------------------------------------------------- Pruefer ---
|
||||
# Jeder Pruefer liefert (wert, anzeige). `wert` wird mit dem letzten Lauf
|
||||
# verglichen, `anzeige` ist die menschenlesbare Fassung.
|
||||
|
||||
def p_github_issue(e):
|
||||
d = hole(f"https://api.github.com/repos/{e['repo']}/issues/{e['nummer']}")
|
||||
st = "geschlossen" if d.get("state") == "closed" else "offen"
|
||||
return st, f"#{e['nummer']} {e['repo']} ist {st} — {d.get('title', '')[:70]}"
|
||||
|
||||
|
||||
def p_github_pr(e):
|
||||
d = hole(f"https://api.github.com/repos/{e['repo']}/pulls/{e['nummer']}")
|
||||
if d.get("merged_at"):
|
||||
st = f"gemergt am {d['merged_at'][:10]}"
|
||||
elif d.get("state") == "closed":
|
||||
st = "geschlossen ohne Merge"
|
||||
else:
|
||||
st = "offen" + (" (Entwurf)" if d.get("draft") else "")
|
||||
return st, f"PR #{e['nummer']} {e['repo']} {st} — {d.get('title', '')[:70]}"
|
||||
|
||||
|
||||
def _release(e):
|
||||
d = hole(f"https://api.github.com/repos/{e['repo']}/releases/latest")
|
||||
return d.get("tag_name", "?"), (d.get("published_at") or "")[:10]
|
||||
|
||||
|
||||
def p_github_release(e):
|
||||
tag, wann = _release(e)
|
||||
return tag, f"{e['repo']} juengster Release {tag} ({wann})"
|
||||
|
||||
|
||||
def p_github_release_major(e):
|
||||
tag, wann = _release(e)
|
||||
m = re.search(r"(\d+)", tag)
|
||||
major = m.group(1) if m else tag
|
||||
return major, f"{e['repo']} Major {major} (Release {tag}, {wann})"
|
||||
|
||||
|
||||
def p_github_branch(e):
|
||||
d = hole(f"https://api.github.com/repos/{e['repo']}/commits?sha={e['branch']}&per_page=1")
|
||||
c = d[0]
|
||||
wann = c["commit"]["committer"]["date"][:10]
|
||||
return c["sha"][:10], f"{e['repo']}@{e['branch']} letzter Commit {wann} — {c['commit']['message'].splitlines()[0][:60]}"
|
||||
|
||||
|
||||
def p_pypi(e):
|
||||
d = hole(f"https://pypi.org/pypi/{e['paket']}/json")
|
||||
v = d["info"]["version"]
|
||||
return v, f"PyPI {e['paket']} = {v}"
|
||||
|
||||
|
||||
def p_hf_author(e):
|
||||
d = hole(f"https://huggingface.co/api/models?author={e['author']}&sort=lastModified&direction=-1&limit={e.get('limit', 15)}")
|
||||
ids = sorted(m["id"] for m in d)
|
||||
return "\n".join(ids), f"{len(ids)} juengste Repos von {e['author']} auf Hugging Face"
|
||||
|
||||
|
||||
def p_url_zeilen(e):
|
||||
roh = hole(e["url"], json_erwartet=False)
|
||||
zeilen = [z.strip() for z in roh.splitlines() if e["muster"] in z][: int(e.get("max_zeilen", 30))]
|
||||
h = hashlib.sha1("\n".join(zeilen).encode()).hexdigest()[:10]
|
||||
return h, f"{len(zeilen)} Zeilen mit '{e['muster']}' in {e['url'].split('/')[2]} (Stand {h})"
|
||||
|
||||
|
||||
PRUEFER = {
|
||||
"github_issue": p_github_issue,
|
||||
"github_pr": p_github_pr,
|
||||
"github_release": p_github_release,
|
||||
"github_release_major": p_github_release_major,
|
||||
"github_branch": p_github_branch,
|
||||
"pypi": p_pypi,
|
||||
"hf_author": p_hf_author,
|
||||
"url_zeilen": p_url_zeilen,
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("UPSTREAM (Watchlist — was hat sich draussen getan?)")
|
||||
try:
|
||||
eintraege = json.loads(WATCHLIST.read_text(encoding="utf-8")).get("eintraege", [])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
warn(f"Watchlist {WATCHLIST} nicht lesbar: {exc}")
|
||||
return 0
|
||||
try:
|
||||
state = json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {}
|
||||
except Exception: # noqa: BLE001
|
||||
state = {}
|
||||
|
||||
for e in eintraege:
|
||||
key, typ = e.get("key", "?"), e.get("typ", "?")
|
||||
pruefer = PRUEFER.get(typ)
|
||||
if not pruefer:
|
||||
warn(f"{key}: unbekannter Typ '{typ}' — Eintrag wird ignoriert")
|
||||
continue
|
||||
try:
|
||||
wert, anzeige = pruefer(e)
|
||||
except urllib.error.HTTPError as exc:
|
||||
warn(f"{key}: nicht abrufbar (HTTP {exc.code})")
|
||||
continue
|
||||
except Exception as exc: # noqa: BLE001
|
||||
warn(f"{key}: nicht abrufbar ({type(exc).__name__}: {str(exc)[:60]})")
|
||||
continue
|
||||
|
||||
alt = state.get(key, {}).get("wert")
|
||||
if alt is None:
|
||||
ok(f"{key}: erstmals erfasst — {anzeige}")
|
||||
elif alt == wert:
|
||||
ok(f"{key}: unveraendert — {anzeige}")
|
||||
else:
|
||||
if typ == "hf_author":
|
||||
neue = sorted(set(wert.split("\n")) - set(alt.split("\n")))
|
||||
anzeige = f"neue Repos von {e['author']}: " + ", ".join(neue[:8]) if neue else anzeige
|
||||
hinweis = e.get("warum", "")
|
||||
neu(f"{key}: {anzeige}" + (f"\n → {hinweis[:220]}" if hinweis else ""))
|
||||
state[key] = {"wert": wert, "anzeige": anzeige}
|
||||
|
||||
try:
|
||||
STATE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = STATE.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(state, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
os.replace(tmp, STATE)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
warn(f"Zustand nicht speicherbar ({exc}) — naechster Lauf meldet alles erneut")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -4,13 +4,19 @@ globalTTL: 0
|
||||
|
||||
models:
|
||||
Qwen3.6-35B-A3B:
|
||||
# parallel 2 + c 131072: 2 Slots à 65k (Slot 2 = Mem0-Lern-Extraktion, blockiert Voice-Turns
|
||||
# parallel 2 + c 131072: 2 Slots à 65k (Slot 2 = Nebenlast/Hintergrund-Extraktion, blockiert Voice-Turns
|
||||
# nicht mehr); KV Q8_0 macht das speicherneutral zu vorher (65k f16). Bench 2026-07-02:
|
||||
# Q8_0 kostet 0 t/s, MTP n-max 3 = +26% vs. ohne Spec. (Q4_K-KV stand mal im Template,
|
||||
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
|
||||
# Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.)
|
||||
# 17.09.2026: Uncensored-Variante (HauhauCS Aggressive, Q4_K_M) mit demselben giocom-DFlash-Draft. Prüfstand: 100,8 t/s
|
||||
# @13k (Original 92,8), Prefill 13,1 s, Tools 11/12 (1 Denk-Ausreißer), Hermes-Smoke grün. Original bleibt in
|
||||
# Qwen3.6-35B-A3B-MTP-GGUF liegen — Rückweg = Pfad zurück.
|
||||
# --reasoning-budget 2048 (17.09.2026): Deckel für Denk-Tokens — die abliterierte Variante hatte im Prüfstand 1/12
|
||||
# Denk-Ausreißer ohne Antwort; mit Deckel wird daraus eine kürzer bedachte Antwort statt Stille. Normale Denkphasen
|
||||
# des Hirns liegen bei ein paar hundert Tokens.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf
|
||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-Uncensored-GGUF/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf --reasoning-budget 2048
|
||||
ttl: 0
|
||||
aliases:
|
||||
- hermes
|
||||
@@ -25,21 +31,29 @@ models:
|
||||
# Qwen 3.8 27B: Dichtes 27B-Modell mit hybrider Linear-Attention (48/64 Schichten linear),
|
||||
# nativem Multimodal-Support (mmproj-BF16) und Tool-Calling via --jinja.
|
||||
# Unser neuer Haupt-Coder mit 262k Kontext, Thinking Mode und extrem sauberer Code-Qualität.
|
||||
# DFlash2-Draft (z-lab, Q4_K_M) seit 04.09.2026: gemessen 12,6 -> 31 t/s (Akzeptanz 0,78, n-max 3 default;
|
||||
# n-max 5 und f16-KV bringen nichts, Q4-Draft = Q8-Draft). Braucht llama.cpp >= 28.08. (PR 27342 + Vulkan-Fix 27812).
|
||||
# 17.09.2026: Uncensored-Variante (JonathanColetti, Heretic-Abliteration: KL 0,12, Refusals 98->12/100, Benchmarks -0,5)
|
||||
# mit demselben DFlash2-Draft (Akzeptanz 0,82 @13k). Prüfstand: 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3,
|
||||
# Hermes-Smoke grün. Original bleibt in Qwen3.8-27B-GGUF liegen — Rückweg = beide Pfade zurück.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3.8-27B-GGUF/Qwen3.8-27B-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3.8-27B-GGUF/mmproj-BF16.gguf --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0
|
||||
llama-server -m /srv/models/Qwen3.8-27B-Uncensored-GGUF/Qwen3.8-27B-Uncensored-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3.8-27B-Uncensored-GGUF/mmproj-Qwen3.8-27B-Uncensored-F16.gguf --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.8-27B-DFlash2-GGUF/Qwen3.8-27B-DFlash2-Q4_K_M.gguf
|
||||
ttl: 5400
|
||||
aliases:
|
||||
- coder
|
||||
- heavy
|
||||
capabilities:
|
||||
in: [text, image]
|
||||
out: [text]
|
||||
tools: true
|
||||
context: 65536
|
||||
# --parallel 1 = der Coder bekommt den ganzen Slot (34a9862, am laufenden Prozess
|
||||
# gegengeprueft). Er hat nur einen Verbraucher; Lucy/explore nutzen hermes, review heavy.
|
||||
context: 131072
|
||||
Muse-Glimmer-30B:
|
||||
# Meta Muse Glimmer 30B: Dichtes 30B-Agenten-Modell mit DFlash-Speculative-Drafting
|
||||
# und 1.8B Perception Multimodal Projector. Ideal als Runtime-Debugger & Fehler-Diagnostiker.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Muse-Glimmer-30B-GGUF/Muse-Glimmer-30B-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Muse-Glimmer-30B-GGUF/mmproj-kquant.gguf --jinja --spec-type draft-dflash --spec-draft-model /srv/models/Muse-Glimmer-30B-GGUF/dflash-kquant.gguf
|
||||
llama-server -m /srv/models/Muse-Glimmer-30B-GGUF/Muse-Glimmer-30B-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Muse-Glimmer-30B-GGUF/mmproj-kquant.gguf --jinja --spec-type draft-dflash --spec-draft-model /srv/models/Muse-Glimmer-30B-GGUF/dflash-kquant.gguf
|
||||
ttl: 600
|
||||
aliases:
|
||||
- debugger
|
||||
@@ -50,7 +64,7 @@ models:
|
||||
context: 65536
|
||||
Qwen3-Embedding-0.6B:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf --host 127.0.0.1 --port ${PORT} --embedding --pooling last -ngl 999 -fa on --no-mmap -c 8192
|
||||
llama-server -m /srv/models/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf --host 127.0.0.1 --port ${PORT} --embedding --pooling last -ngl 999 -fa on --load-mode none -c 8192
|
||||
ttl: 0
|
||||
aliases:
|
||||
- embed
|
||||
@@ -58,10 +72,10 @@ models:
|
||||
# heavy (B5-Entscheid 03.07.) + nächtlicher CHEF-GUTACHTER (04:30-Cron). 60 GB —
|
||||
# passt seit der Warm-Set-Diät (07.07., vision on-demand) wieder NEBEN Hirn+embed.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/gpt-oss-120b-GGUF/gpt-oss-120b-mxfp4-00001-of-00003.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256 -cram 16384
|
||||
llama-server -m /srv/models/gpt-oss-120b-GGUF/gpt-oss-120b-mxfp4-00001-of-00003.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --load-mode none --jinja --cache-reuse 256 -cram 16384
|
||||
ttl: 600
|
||||
aliases:
|
||||
- heavy
|
||||
# 04.09.2026: Rolle `heavy` an Qwen3.8-27B abgegeben (AA-Index 52 vs 24, 17 statt 60 GB, 31 t/s mit DFlash2).
|
||||
# Ohne Alias = Rollback-Reserve, direkt als `gpt-oss-120b` ansprechbar.
|
||||
capabilities:
|
||||
in: [text]
|
||||
out: [text]
|
||||
@@ -69,7 +83,7 @@ models:
|
||||
context: 32768
|
||||
Qwen3-VL-30B-A3B-Instruct:
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
||||
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --load-mode none --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
||||
ttl: 900
|
||||
aliases:
|
||||
- vision
|
||||
@@ -78,11 +92,11 @@ models:
|
||||
out: [text]
|
||||
context: 32768
|
||||
Qwen3-Reranker-0.6B:
|
||||
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Mem0-Suchtreffer nach echter
|
||||
# Gedächtnis-Zweitstufe (07.07., Rollen-Audit): ordnet Gedächtnis-Suchtreffer nach echter
|
||||
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
||||
# Winzig (~0,7 GB) → in brains (verdrängungssicher); lädt in ~1-2 s, erste Anfrage wärmt.
|
||||
cmd: |
|
||||
llama-server -m /srv/models/Qwen3-Reranker-0.6B-GGUF/Qwen3-Reranker-0.6B-q8_0.gguf --host 127.0.0.1 --port ${PORT} --reranking --pooling rank --embedding -ngl 999 -fa on --no-mmap -c 8192
|
||||
llama-server -m /srv/models/Qwen3-Reranker-0.6B-GGUF/Qwen3-Reranker-0.6B-q8_0.gguf --host 127.0.0.1 --port ${PORT} --reranking --pooling rank --embedding -ngl 999 -fa on --load-mode none -c 8192
|
||||
ttl: 0
|
||||
aliases:
|
||||
- reranker
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Auftragsbuch: einen LUCY-Vorschlags-Branch ANNEHMEN — das PC-Gegenstück zu auftrag-annehmen.sh.
|
||||
# Lucy (Electron) lebt auf dem Windows-PC (F:\Coding Stuff\lucy); dist liegt NICHT im Repo.
|
||||
# Annehmen heißt darum: Merge auf main (hier, Box) → der PC zieht main, baut dist neu und
|
||||
# startet Lucy neu, WENN sie lief (User-Entscheid: Lucy startet nur, wenn der Commander sie will).
|
||||
# Schlägt am PC irgendetwas fehl, wird der Merge auf main automatisch revertiert — die laufende
|
||||
# Lucy bleibt die alte (der PC-Runner stellt dist aus seinem Backup wieder her).
|
||||
#
|
||||
# Der PC wird über den PC-Executor (client/hermes-pc/executor.py, :7777, Bearer-Token aus
|
||||
# ~/.hermes/config.yaml) gesteuert. /shell hat 60 s Timeout → der Build läuft DETACHED
|
||||
# (deploy/lucy-annahme.ps1 im Lucy-Repo schreibt eine Status-Datei, wir pollen sie).
|
||||
#
|
||||
# Läuft wie der MC2-Runner als EIGENE systemd-Unit und als /tmp-Kopie (Selbst-Reset-Falle).
|
||||
# Status-Schlüssel in der Auftragsbuch-JSON: "lucy:<branch>".
|
||||
#
|
||||
# Nutzung: lucy-annahme.sh <branch> (z. B. wartung/turncheck-default-aus)
|
||||
set -uo pipefail
|
||||
|
||||
BRANCH="${1:?Nutzung: lucy-annahme.sh <branch>}"
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
LUCY="${MC2_LUCY_DIR:-$HOME/lucy}"
|
||||
STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}"
|
||||
HERMES_CONF="${HERMES_CONF:-$HOME/.hermes/config.yaml}"
|
||||
SLUG="lucy-$(echo "$BRANCH" | tr '/' '-')"
|
||||
WT="/tmp/annahme-$SLUG"
|
||||
LOG="/tmp/annahme-$SLUG.log"
|
||||
|
||||
# Windows-Seite (Pfade im PC-Dateisystem; einfach gehalten — EIN PC, EIN Checkout).
|
||||
PC_REPO='F:\Coding Stuff\lucy'
|
||||
PC_STATUSFILE='F:\Coding Stuff\lucy\.lucy-annahme.json'
|
||||
PC_RUNNER='F:\Coding Stuff\lucy\deploy\lucy-annahme.ps1'
|
||||
BUILD_TIMEOUT_S=1500 # 25 min Deckel für npm ci + tsc + electron-builder + Stimm-Warmup
|
||||
|
||||
notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; }
|
||||
|
||||
# Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur).
|
||||
status(){ # $1=state $2=detail
|
||||
python3 - "$STATUS" "lucy:$BRANCH" "$1" "$2" <<'PY'
|
||||
import json, os, sys, time
|
||||
p, key, state, detail = sys.argv[1:5]
|
||||
try:
|
||||
d = json.load(open(p, encoding="utf-8"))
|
||||
except Exception:
|
||||
d = {}
|
||||
d.setdefault("branches", {})[key] = {"state": state, "detail": detail, "ts": time.time()}
|
||||
tmp = p + ".tmp"
|
||||
json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False)
|
||||
os.replace(tmp, p)
|
||||
PY
|
||||
}
|
||||
|
||||
# PC-Executor: PowerShell-Kommando ausführen. stdout des PCs → stdout; rc 0 nur wenn
|
||||
# Transport UND Kommando grün. Token/URL kommen aus der Hermes-Config (kein zweiter Ablageort).
|
||||
PC_URL="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_URL:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')"
|
||||
PC_TOKEN="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_TOKEN:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')"
|
||||
pc_shell(){ # $1 = PowerShell-Kommando
|
||||
python3 - "$PC_URL" "$PC_TOKEN" "$1" <<'PY'
|
||||
import json, sys, urllib.request
|
||||
url, token, cmd = sys.argv[1:4]
|
||||
req = urllib.request.Request(url + "/shell", data=json.dumps({"command": cmd}).encode(),
|
||||
headers={"Authorization": "Bearer " + token,
|
||||
"Content-Type": "application/json"})
|
||||
try:
|
||||
r = json.load(urllib.request.urlopen(req, timeout=58))
|
||||
except Exception as e:
|
||||
print(f"(PC-Executor nicht erreichbar: {e})")
|
||||
sys.exit(2)
|
||||
out = (r.get("stdout") or "").strip()
|
||||
if out:
|
||||
print(out)
|
||||
err = (r.get("stderr") or "").strip()
|
||||
if err:
|
||||
print(err, file=sys.stderr)
|
||||
sys.exit(0 if r.get("returncode") == 0 else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
cleanup_wt(){ git -C "$LUCY" worktree remove --force "$WT" 2>/dev/null || true; }
|
||||
|
||||
fail(){
|
||||
status "fehlgeschlagen" "$1"
|
||||
notify "Lucy-Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen."
|
||||
cleanup_wt
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Revert nach schon gepushtem Merge (PC-Phase rot): main zurückdrehen, PC-Checkout nachziehen.
|
||||
revert_main(){ # $1 = Grund für die Meldung
|
||||
status "rollback" "PC-Phase rot — Merge wird automatisch zurückgedreht"
|
||||
git -C "$LUCY" fetch -q origin || true
|
||||
cleanup_wt
|
||||
if git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 \
|
||||
&& git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \
|
||||
&& git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then
|
||||
cleanup_wt
|
||||
# PC-Checkout best-effort auf den revertierten Stand nachziehen (Quelle = laufende Lucy).
|
||||
pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 || true
|
||||
status "zurueckgerollt" "$1 — Merge automatisch revertiert, Lucy läuft in der alten Version weiter"
|
||||
notify "Lucy-Vorschlag '$BRANCH' hat den PC-Build/Neustart GERISSEN: $1. Merge automatisch zurückgedreht — Lucy läuft in der alten Version weiter. Der Branch bleibt zur Analyse liegen. Log: $LOG"
|
||||
exit 1
|
||||
fi
|
||||
cleanup_wt
|
||||
status "kritisch" "Revert fehlgeschlagen — Lucy-Repo und PC von Hand prüfen"
|
||||
notify "KRITISCH: Lucy-Annahme '$BRANCH' scheiterte UND der automatische Revert schlug fehl — bitte Lucy-Repo (Gitea main) und PC prüfen. Log: $LOG"
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ -n "$PC_URL" ] && [ -n "$PC_TOKEN" ] || fail "PC-Executor-Zugang fehlt in $HERMES_CONF (PC_EXECUTOR_URL/TOKEN)"
|
||||
|
||||
# ── Vorprüfungen am PC (VOR dem Merge — solange ist nichts kaputtzumachen) ──
|
||||
status "laeuft" "Vorprüfung: ist der PC bereit?"
|
||||
curl -sf -m 8 "$PC_URL/health" >/dev/null 2>&1 \
|
||||
|| fail "PC-Executor nicht erreichbar — Lucy-Annahme braucht den eingeschalteten PC (Executor-Task läuft bei Anmeldung)"
|
||||
|
||||
PC_BRANCH="$(pc_shell "git -C '$PC_REPO' rev-parse --abbrev-ref HEAD" 2>>"$LOG")" \
|
||||
|| fail "PC-Arbeitskopie nicht lesbar (git am PC): $PC_BRANCH"
|
||||
[ "$PC_BRANCH" = "main" ] || fail "PC-Arbeitskopie steht auf '$PC_BRANCH' statt main — bitte am PC aufräumen"
|
||||
|
||||
PC_DIRTY="$(pc_shell "git -C '$PC_REPO' status --porcelain -uno" 2>>"$LOG")" \
|
||||
|| fail "PC-Arbeitskopie nicht prüfbar"
|
||||
[ -z "$PC_DIRTY" ] || fail "PC-Arbeitskopie hat ungespeicherte Änderungen — bitte erst am PC committen/aufräumen: $(echo "$PC_DIRTY" | head -3 | tr '\n' ' ')"
|
||||
|
||||
WAS_RUNNING="$(pc_shell "if (Get-Process Lucy -ErrorAction SilentlyContinue) { '1' } else { '0' }" 2>>"$LOG")" || WAS_RUNNING="0"
|
||||
case "$WAS_RUNNING" in 1|0) ;; *) WAS_RUNNING="0" ;; esac
|
||||
|
||||
# ── Merge auf main (Box, isolierter Worktree — wie beim MC2-Runner) ─────────
|
||||
status "laeuft" "Merge wird vorbereitet"
|
||||
git -C "$LUCY" fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen"
|
||||
git -C "$LUCY" rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \
|
||||
|| fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)"
|
||||
|
||||
# Kaputt aufgesetzte Branches (kein gemeinsamer Vorfahre) → Merge kann NIE gelingen.
|
||||
git -C "$LUCY" merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \
|
||||
|| fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben"
|
||||
|
||||
cleanup_wt
|
||||
git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden"
|
||||
MERGE_MSG="Auftragsbuch: Lucy-Vorschlag '$BRANCH' angenommen (Ein-Klick-Gate)"
|
||||
if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then
|
||||
git -C "$WT" merge --abort 2>/dev/null || true
|
||||
# Selbstheilung: veralteten Branch mechanisch auf main rebasen (wie im MC2-Runner).
|
||||
status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht"
|
||||
WT2="/tmp/annahme-rebase-$SLUG"
|
||||
git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true
|
||||
REBASED=""
|
||||
if git -C "$LUCY" worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \
|
||||
&& git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
rebase origin/main >>"$LOG" 2>&1; then
|
||||
REBASED="$(git -C "$WT2" rev-parse HEAD)"
|
||||
else
|
||||
git -C "$WT2" rebase --abort 2>/dev/null || true
|
||||
fi
|
||||
git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true
|
||||
[ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)"
|
||||
git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \
|
||||
merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \
|
||||
|| { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; }
|
||||
fi
|
||||
|
||||
status "laeuft" "Push nach main läuft"
|
||||
PUSHED=0
|
||||
for _ in 1 2 3; do
|
||||
git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; }
|
||||
sleep 5
|
||||
done
|
||||
[ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)"
|
||||
cleanup_wt
|
||||
|
||||
# ── PC: main ziehen, Build detached starten ─────────────────────────────────
|
||||
status "laeuft" "PC zieht den neuen Stand"
|
||||
pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; if (\$LASTEXITCODE -ne 0) { exit 1 }; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 \
|
||||
|| revert_main "PC konnte main nicht ziehen (git fetch/merge am PC rot)"
|
||||
|
||||
pc_shell "if (Test-Path '$PC_RUNNER') { exit 0 } else { exit 1 }" >>"$LOG" 2>&1 \
|
||||
|| revert_main "PC-Runner-Skript fehlt im Lucy-Repo (deploy/lucy-annahme.ps1)"
|
||||
|
||||
# WICHTIG: -File-Pfad MUSS eingebettete Anführungszeichen tragen — Start-Process fügt
|
||||
# die ArgumentList-Elemente OHNE Quoting zusammen; 'F:\Coding Stuff\…' zerbricht sonst am
|
||||
# Leerzeichen und die gespawnte powershell stirbt still (erster Live-Lauf, 12.07.).
|
||||
pc_shell "Remove-Item -Force '$PC_STATUSFILE' -ErrorAction SilentlyContinue; Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"$PC_RUNNER\"','-WasRunning','$WAS_RUNNING'" >>"$LOG" 2>&1 \
|
||||
|| revert_main "PC-Build ließ sich nicht starten"
|
||||
|
||||
# ── Status-Datei pollen, Fortschritt in die UI spiegeln ─────────────────────
|
||||
status "laeuft" "PC baut Lucy (dist) — das dauert ein paar Minuten"
|
||||
DEADLINE=$(( $(date +%s) + BUILD_TIMEOUT_S ))
|
||||
LAST_DETAIL=""
|
||||
FINAL=""
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
sleep 10
|
||||
RAW="$(pc_shell "if (Test-Path '$PC_STATUSFILE') { Get-Content -Raw '$PC_STATUSFILE' }" 2>>"$LOG")" || continue
|
||||
[ -n "$RAW" ] || continue
|
||||
# Parse OHNE f-String-Escapes: \" in f-String-Ausdruecken ist seit Python 3.12/3.14
|
||||
# ein SyntaxError (alter Pre-3.12-Stil) — genau das legte den ersten Live-Lauf lahm.
|
||||
PARSED="$(printf '%s' "$RAW" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.loads(sys.stdin.read())
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
detail = str(d.get("detail", "")).replace("|", "/").replace("\n", " ")
|
||||
done = "1" if d.get("done") else "0"
|
||||
ok = "1" if d.get("ok") else "0"
|
||||
print(done + "|" + ok + "|" + detail)
|
||||
')"
|
||||
[ -n "$PARSED" ] || continue
|
||||
DONE="${PARSED%%|*}"; REST="${PARSED#*|}"; OK="${REST%%|*}"; DETAIL="${REST#*|}"
|
||||
if [ "$DETAIL" != "$LAST_DETAIL" ] && [ -n "$DETAIL" ]; then
|
||||
LAST_DETAIL="$DETAIL"
|
||||
status "laeuft" "PC: $DETAIL"
|
||||
fi
|
||||
if [ "$DONE" = "1" ]; then FINAL="$OK"; break; fi
|
||||
done
|
||||
|
||||
[ -n "$FINAL" ] || revert_main "PC-Build-Timeout nach $((BUILD_TIMEOUT_S/60)) Minuten (Status-Datei blieb offen)"
|
||||
[ "$FINAL" = "1" ] || revert_main "${LAST_DETAIL:-PC meldet Fehler ohne Detail}"
|
||||
|
||||
# ── Grün: Branch aufräumen, ehrlich melden ──────────────────────────────────
|
||||
git -C "$LUCY" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true
|
||||
git -C "$LUCY" fetch -q --prune origin 2>/dev/null || true
|
||||
status "eingespielt" "${LAST_DETAIL:-Build grün}"
|
||||
if [ "$WAS_RUNNING" = "1" ]; then
|
||||
notify "Lucy-Vorschlag '$BRANCH' ist LIVE — dist am PC neu gebaut, Lucy neu gestartet, Stimme meldet sich. ✅"
|
||||
else
|
||||
notify "Lucy-Vorschlag '$BRANCH' ist eingespielt — dist am PC neu gebaut. Lucy war aus und bleibt aus; die neue Version startet beim nächsten Öffnen. ✅"
|
||||
fi
|
||||
exit 0
|
||||
@@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=MC2 Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config)
|
||||
Description=MC2 Zustands-Backup (Hermes-Configs/Secrets + llama-swap config)
|
||||
Documentation=file:%h/mission-control-v2/docs/BACKUP.md
|
||||
|
||||
[Service]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# systemd-USER-Unit für den MC2-Steward — die Wächter-Loops als eigener Prozess (UMBAU v3 P2).
|
||||
# Re-Warm-Wächter und Health-Sentry überleben damit jeden Steuerpult-Neustart, und der Sentry
|
||||
# kann auch das Steuerpult SELBST + den mc2-gateway überwachen (MC_SENTRY_WATCH_MC2=1).
|
||||
# Meldungen gehen per HTTP in den MC2-Briefkasten (MC_ANNOUNCE_HTTP); ist MC2 down, greift
|
||||
# weiterhin der Telegram-Direktweg (notify.sh).
|
||||
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
|
||||
#
|
||||
# ‼️ NACHGETRAGEN 27.08.2026: Diese Unit lief auf der Box, fehlte aber im Repo — sie war weder
|
||||
# versioniert noch vom Disaster-Recovery abgedeckt, obwohl backend/routers/system.py,
|
||||
# services/maintenance.py (USER_SERVICES) und deploy/stack-postcheck.sh sie voraussetzen.
|
||||
# Inhalt 1:1 von der laufenden Box übernommen (`systemctl --user cat mc2-steward`), nur die
|
||||
# Description um den entfallenen Mem0-Dedupe-Loop gekürzt. Das Warm-Set-Drop-in liegt
|
||||
# daneben in mc2-steward.service.d-warmset.conf — beim Ausrollen mitnehmen.
|
||||
#
|
||||
# Ablage: ~/.config/systemd/user/mc2-steward.service ; dann:
|
||||
# systemctl --user daemon-reload && systemctl --user enable --now mc2-steward
|
||||
|
||||
[Unit]
|
||||
Description=MC2 Steward (Wächter: Re-Warm, Health-Sentry)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/backend
|
||||
ExecStart=%h/mission-control-v2/backend/.venv/bin/python steward.py
|
||||
Environment=PYTHONPATH=%h/mission-control-v2
|
||||
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||
Environment=MC_MODELS_DIR=/srv/models
|
||||
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
|
||||
Environment=MC_SENTRY_WATCH_MC2=1
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,11 @@
|
||||
# Drop-in für mc2-steward.service — Ablage auf der Box:
|
||||
# ~/.config/systemd/user/mc2-steward.service.d/warmset.conf
|
||||
#
|
||||
# 26.07.2026 — Ko-Residenz ist nicht Warm-Pflicht. Die Gruppe `brains` enthaelt seit dem
|
||||
# Umbau auch Modelle MIT TTL (Coder 90 min, Kritiker 30 min); ohne diese Liste hielte der
|
||||
# Waechter sie faelschlich fuer dauerhaft faellig und arbeitete gegen die TTLs.
|
||||
# ANFUEHRUNGSZEICHEN SIND PFLICHT: systemd trennt Environment= an Leerzeichen, ohne sie
|
||||
# kommt nur das erste Modell an (live erlebt) — das Warm-Ziel waere still zu klein.
|
||||
[Service]
|
||||
Environment="MC_WARMSET=Qwen3-Embedding-0.6B Qwen3-Reranker-0.6B Qwen3.6-35B-A3B"
|
||||
Environment=MC_WARMUP_RERANK=reranker
|
||||
@@ -19,19 +19,16 @@ Environment=MC_PORT=9001
|
||||
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||
Environment=MC_MODELS_DIR=/srv/models
|
||||
# Geteiltes Gedächtnis = die bestehende v1-DB (Kontinuität bis/über Cutover).
|
||||
Environment=MC_MEMORY_DB=/srv/models/mission-control-memory.db
|
||||
# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess
|
||||
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
||||
# bedient /v1 wieder selbst.
|
||||
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
||||
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry, Mem0-Dedupe)
|
||||
# laufen jetzt im eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese drei
|
||||
# Zeilen entfernen (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
||||
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry) laufen jetzt im
|
||||
# eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese zwei Zeilen entfernen
|
||||
# (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
||||
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
||||
Environment=MC_REWARM_ENABLED=0
|
||||
Environment=MC_SENTRY_ENABLED=0
|
||||
Environment=MC_MEM_DEDUPE_ENABLED=0
|
||||
# KEIN MC_ENGINE_UPDATE_CMD-Override mehr: Das alte /usr/local/bin/update-llamacpp zog den
|
||||
# ROCm-Build nach /opt/llamacpp (totes Rollback-Dir) statt des aktiven Vulkan-Builds → Updates
|
||||
# liefen ins Leere ("DONE", aber nichts passierte). Ohne Override nutzt das Backend den Default
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"$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 (= Qwen3.8-27B, seit 04.09. mit DFlash2)",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 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
|
||||
@@ -0,0 +1,91 @@
|
||||
<#
|
||||
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 ""
|
||||
+6
-1
@@ -18,7 +18,10 @@ DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
SERVICES="mission-control-2 hermes-gateway"
|
||||
# Alle User-Dienste, die von wiederhergestellter Config abhaengen. mc2-gateway (der
|
||||
# /v1-Denkpfad) und mc2-steward (Waechter) fehlten hier bis 27.08.2026 — nach einem
|
||||
# Restore liefen beide mit der ALTEN llama-swap-/Hermes-Config weiter.
|
||||
SERVICES="mission-control-2 mc2-gateway mc2-steward hermes-gateway"
|
||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||
@@ -117,6 +120,8 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
|
||||
echo "→ Dienste stoppen…"
|
||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
||||
|
||||
# Alte Snapshots (vor 27.08.2026) tragen noch ein Gedaechtnis-Verzeichnis mit — es wird
|
||||
# bewusst NICHT mehr zurueckgespielt, der zugehoerige Dienst existiert nicht mehr.
|
||||
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
||||
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||
|
||||
@@ -97,21 +97,9 @@ for x in sorted(off, key=lambda x: x.get("title") or ""):
|
||||
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:100]))
|
||||
' 2>/dev/null || echo "(Queue nicht lesbar)"
|
||||
echo
|
||||
echo "## Offene Karten im Auftragsbuch"
|
||||
curl -sf -m 10 "$API/api/auftragsbuch" 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
items = json.load(sys.stdin).get("items") or []
|
||||
except Exception:
|
||||
items = []
|
||||
if not items:
|
||||
print("- (keine)")
|
||||
for it in sorted(items, key=lambda x: x.get("branch") or ""):
|
||||
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
|
||||
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
|
||||
echo
|
||||
echo "## Feste Leitplanken (Kurzfassung — Details docs/wissen/)"
|
||||
echo "- Jede Code-Zeile geht NUR ueber eine Auftragsbuch-Karte live (Klick des Commanders)."
|
||||
echo "- Jede Code-Zeile geht NUR als Branch nach Gitea live: Ampel gruen, Merge durch den Commander, deploy.sh."
|
||||
echo "- TABU ohne explizites User-Ja: Security (approvals/Tokens/ufw/sudoers), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode."
|
||||
echo "- Updates macht der Auto-Update-Timer mit Fangnetz — niemand sonst."
|
||||
} > "$STECK.tmp" 2>/dev/null
|
||||
|
||||
@@ -83,8 +83,8 @@ heavy-Rollen einer Runde am Stueck (heavy bleibt warm), DANN der Skeptiker (ein
|
||||
dieses Projekt nicht (noetige Komponente gibt es nur als Image o. ae.), sag das im Konzept
|
||||
AUSDRUECKLICH mit Begruendung — still ignorieren gilt nicht.
|
||||
- **★ Explizite Commander-Wahl UEBERSTIMMT deine Einschaetzung.** Steht im Auftrag eine Zeile
|
||||
„AUFWAND (vom Commander gewaehlt): SIMPEL …" bzw. „… GRUENDLICH …" (der Commander hat im
|
||||
Auftragsbuch „IDE: simpel" oder „IDE: gruendlich" geklickt), dann HALTE DICH DARAN — SIMPEL =
|
||||
„AUFWAND (vom Commander gewaehlt): SIMPEL …" bzw. „… GRUENDLICH …" (der Commander hat in der
|
||||
Ideen-Ansicht „IDE: simpel" oder „IDE: gruendlich" gewaehlt), dann HALTE DICH DARAN — SIMPEL =
|
||||
2 Rollen/1 Runde, GRUENDLICH = volle 3-5 Rollen/bis 3 Runden — auch wenn deine eigene
|
||||
Schaetzung anders ausfaellt. Nur wenn KEINE solche Zeile da ist, entscheidest du selbst.
|
||||
- **Max 3 Denk-Runden — keine Endlosschleife.** Nach der letzten Runde (je nach Groesse 1-3) kommt
|
||||
|
||||
@@ -17,5 +17,5 @@ Dieser Skill fungiert als dein persönlicher, KI-gesteuerter Sysadmin, der dir j
|
||||
1. **System-Metriken abrufen**: Ruft den Health-Status und Load der Box ab.
|
||||
2. **Logs durchkämmen**: Sucht nach Auffälligkeiten, Fehlern oder Crashes in der Nacht.
|
||||
3. **News Fetching**: Liest die Top-Posts von Hacker News (oder anderen Tech-Feeds), filtert sie nach AI-Relevanz und fasst die 3 wichtigsten Entwicklungen zusammen.
|
||||
4. **Auftragsbuch Check**: Prüft, ob neue unentschiedene Vorschläge im Auftragsbuch liegen.
|
||||
4. **Offene Branches**: Nennt ungemergte Vorschlags-Branches auf Gitea (wartung/*, feature/*, doku/*), falls welche warten.
|
||||
5. **Report Generierung**: Bündelt all diese Informationen in eine kompakte, handyfreundliche (sehr kurze) Nachricht und schickt sie via Telegram an den Commander.
|
||||
|
||||
@@ -10,7 +10,7 @@ set -uo pipefail
|
||||
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
|
||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Gedächtnis)
|
||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Reranker-Stufe)
|
||||
fail=0
|
||||
|
||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||
@@ -47,14 +47,14 @@ else
|
||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||
fi
|
||||
|
||||
# 3b. Embedding-Modell (für Gedächtnis) lädt und liefert einen Vektor?
|
||||
# 3b. Embedding-Modell lädt und liefert einen Vektor?
|
||||
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||
else
|
||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Gedächtnis betroffen)"; fail=1
|
||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht"; fail=1
|
||||
fi
|
||||
|
||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
||||
@@ -84,6 +84,9 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# (Schritt 5 „Gedächtnis-Sidecar erreichbar" entfiel am 27.08.2026: der Dienst auf :8765 ist
|
||||
# seit der Ablösung im August tot, der Check konnte nur noch rot werden. docs/wissen/VERDIKTE.md)
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
echo "=== STACK OK ✓ ==="
|
||||
else
|
||||
|
||||
@@ -1,51 +1,73 @@
|
||||
{
|
||||
"_hinweis": "Living Watchlist des Trend-Radars. Diese Datei MUSS autonom vom Trend-Radar Agenten umgeschrieben werden, wenn er neue Quellen findet oder alte obsolet werden.",
|
||||
"_hinweis": "Watchlist des Stack-Radars. Wird von deploy/jobs/stack-upstream.py (Skript, kein Modell) samstags abgefragt; jeder Eintrag = ein abrufbarer Wert, verglichen mit dem letzten Lauf. Typen: github_issue, github_pr, github_release, github_release_major, github_branch, pypi, hf_author, url_zeilen. `warum` wird bei NEU mitgemeldet — dort steht, was dann zu tun ist.",
|
||||
"eintraege": [
|
||||
{
|
||||
"key": "mmq-prefill-gfx1151",
|
||||
"typ": "github_issue",
|
||||
"key": "qwen-modelle",
|
||||
"typ": "hf_author",
|
||||
"author": "Qwen",
|
||||
"limit": 20,
|
||||
"warum": "Lucys Hirn ist Qwen3.6-35B-A3B, der Coder Qwen3.8-27B. Ein neues A3B-Modell waere der Hirn-Kandidat (dichte Modelle nie, Verdikt 17.07.); ein neues 27B/30B der Coder-Kandidat. Immer erst auf der Box messen."
|
||||
},
|
||||
{
|
||||
"key": "zlab-drafts",
|
||||
"typ": "hf_author",
|
||||
"author": "z-lab",
|
||||
"limit": 20,
|
||||
"warum": "z-lab baut die DFlash-Entwurfsmodelle. DFlash2 brachte dem Coder 12,6 -> 31 t/s (04.09.). Ein DFlash2 fuer Qwen3.6-35B-A3B waere derselbe Hebel fuers Hirn — DFlash v1 faellt bei 32k Tiefe auf 1,02x."
|
||||
},
|
||||
{
|
||||
"key": "flash-next-mtp",
|
||||
"typ": "github_pr",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"nummer": 21284,
|
||||
"bedingung": "Issue geschlossen = MMQ-Tuning für gfx1151 vermutlich gemerged",
|
||||
"warum": "Getunte MMQ-Defaults brachten +60 % ROCm-Prefill auf 35B-MoE (unser Hirn-Typ) und +20 % auf 122B. Wenn gemerged: ROCm-Langkontext-Bench für coder fällig (Vulkan-Verdikt bleibt bis zur Messung)."
|
||||
"nummer": 28243,
|
||||
"warum": "MTP fuer Qwen3.8-Flash-Next (125B-A6B). Erst mit Merge + Vulkan-Zahlen lohnt ein Blick — und nur, wenn eine Quantisierung neben das Warm-Set passt (Regel: Warm-Set + Modell <= 115 GB; UD-IQ4_XS = 94 GB passt NICHT)."
|
||||
},
|
||||
{
|
||||
"key": "hrx-backend",
|
||||
"typ": "github_branch",
|
||||
"repo": "AMD-Ecosystem/llama.cpp",
|
||||
"branch": "hrx-graph-develop-v2",
|
||||
"warum": "AMDs HRX-Runtime (Lemonade 11.9): gegen HIP +21-25 % Decode, +20-123 % Prefill, also etwa Vulkan-Niveau beim Decode und vorn beim Prefill. Stand 04.09. nur Qwen3-30B-A3B/Llama-3/Gemma-3. Interessant, sobald Qwen3.6/3.8 und Spec-Decoding laufen — dann Box-Bench gegen Vulkan."
|
||||
},
|
||||
{
|
||||
"key": "rocm-releases",
|
||||
"typ": "github_release",
|
||||
"repo": "ROCm/ROCm",
|
||||
"bedingung": "Neuer ROCm-Release seit letzter Meldung",
|
||||
"warum": "Seit 7.14 ist TheRock der Produktions-Stack und AMD optimiert offiziell für die Halo-Familie — ROCm könnte Vulkan bei der Token-Erzeugung einholen. Bei neuem Release: Community-Grid-Zeilen unten gegenlesen."
|
||||
"warum": "ROCm 10.0 (27.08.) hat das Vulkan-Verdikt nicht gekippt: HIP gewinnt Prefill, Vulkan Decode. Bei neuem Release: Strix-Halo-Community-Grid gegenlesen, erst bei tg-Trendwende Box-Bench (Lychee-Technology liefert fertige ROCm-Builds)."
|
||||
},
|
||||
{
|
||||
"key": "mmq-prefill-gfx1151",
|
||||
"typ": "github_issue",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"nummer": 21284,
|
||||
"warum": "Getunte MMQ-Defaults brachten +60 % ROCm-Prefill auf 35B-MoE (unser Hirn-Typ). Wenn geschlossen: ROCm-Langkontext-Bench fuer coder faellig (Vulkan-Verdikt bleibt bis zur Messung)."
|
||||
},
|
||||
{
|
||||
"key": "strix-halo-grid",
|
||||
"typ": "url_zeilen",
|
||||
"url": "https://raw.githubusercontent.com/hogeheer499-commits/strix-halo-guide/main/README.md",
|
||||
"muster": "t/s",
|
||||
"max_zeilen": 30,
|
||||
"warum": "Der ehrlichste externe Messpunkt auf unserer Hardware. Bei Aenderung: auf Vulkan<->ROCm-Trendwende und neue Spec-Decoding-Verfahren achten."
|
||||
},
|
||||
{
|
||||
"key": "electron-major",
|
||||
"typ": "github_release_major",
|
||||
"repo": "electron/electron",
|
||||
"bedingung": "Neue MAJOR-Version seit letzter Meldung",
|
||||
"warum": "Lucys Desktop-Shell am PC. Versionsstand NIE dieser Notiz glauben — IMMER live pruefen: lucy-desktop/package.json + node_modules/electron (22.07.: 43.2.0 = aktueller Stable; der fruehere '33'-Stand hier war veraltet und produzierte einen Fehlalarm im Rundumschlag). Neue Major = Chromium-Jahrgang Sicherheitspatches; Update = npm install + npm run dist am PC, bleibt Handarbeit; der Radar erinnert nur."
|
||||
"warum": "Lucys Desktop-Shell am PC. Versionsstand IMMER live pruefen (lucy-desktop/package.json), nie dieser Notiz glauben. Neue Major = Chromium-Sicherheitspatches; Update = npm install + npm run dist am PC, bleibt Handarbeit."
|
||||
},
|
||||
{
|
||||
"key": "pocket-tts",
|
||||
"typ": "pypi",
|
||||
"paket": "pocket-tts",
|
||||
"bedingung": "Neue Version auf PyPI",
|
||||
"warum": "Lucys Stimme am PC (installiert 2.1.0, german_24l) — neue Versionen können Stimm-/Pausen-Fixes bringen; Update bleibt Handarbeit am PC (pocket = DIE Stimme, Verdikt 16.07.)."
|
||||
"warum": "Lucys Stimme am PC (2.1.0, german_24l). Neue Versionen koennen Stimm-/Pausen-Fixes bringen; Update bleibt Handarbeit am PC (pocket = DIE Stimme, Verdikt 16.07.)."
|
||||
},
|
||||
{
|
||||
"key": "lmstudio-bionic",
|
||||
"typ": "github_issue",
|
||||
"repo": "lmstudio-ai/lmstudio-bug-tracker",
|
||||
"nummer": 2185,
|
||||
"bedingung": "Issue geschlossen = Bionic-Linux-Port vermutlich da",
|
||||
"warum": "LM Studio Bionic (eigenstaendige Agent-App, Preview seit 16.07.26, Win+Mac, gratis, closed source). Stand 22.07.: KEINE eigenen OpenAI-Endpunkte anbindbar (Box :9001 bleibt draussen — nur In-App-Modelle, LM Link mit Konto+Tailscale-Mesh oder Secure Cloud), keine CLI/API/Headless. Fuer den Stack erst interessant bei: Linux-Port (dieses Issue), Custom-Endpoint-Support oder CLI — dazu Changelog lmstudio.ai/changelog gegenlesen; llmster auf der Box waere der LM-Link-Rechenknoten-Weg (seit 0.4.20), dupliziert aber die eigene Engine."
|
||||
},
|
||||
{
|
||||
"key": "llamacpp-rocm-diskussion",
|
||||
"typ": "url_zeilen",
|
||||
"url": "https://raw.githubusercontent.com/hogeheer499-commits/strix-halo-guide/main/README.md",
|
||||
"muster": "t/s",
|
||||
"max_zeilen": 30,
|
||||
"bedingung": "Community-Messwerte (Vulkan vs. ROCm, MTP, neue Beschleuniger) auf unserer Hardware",
|
||||
"warum": "Der Strix-Halo-Community-Grid ist der ehrlichste externe Messpunkt. Auf tg-Trendwende Vulkan↔ROCm und neue Spec-Decoding-Verfahren achten."
|
||||
"warum": "LM Studio Bionic: erst interessant bei Linux-Port (dieses Issue), Custom-Endpoint-Support oder CLI. Bis dahin bleibt die Box :9001 draussen."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# systemd-USER-Unit für den MC2 Voice-Sidecar — lokales STT + gestuftes TTS.
|
||||
#
|
||||
# ‼️ NACHGETRAGEN 27.08.2026: Diese Unit lief auf der Box, fehlte aber im Repo — weder
|
||||
# versioniert noch vom Disaster-Recovery abgedeckt, obwohl services/maintenance.py
|
||||
# (USER_SERVICES) und routers/system.py sie voraussetzen. Inhalt 1:1 von der laufenden Box
|
||||
# übernommen (`systemctl --user cat voice-service`).
|
||||
#
|
||||
# Ablage: ~/.config/systemd/user/voice-service.service ; dann:
|
||||
# systemctl --user daemon-reload && systemctl --user enable --now voice-service
|
||||
|
||||
[Unit]
|
||||
Description=MC2 Voice Sidecar — lokales STT (faster-whisper) + gestuftes TTS (Piper/Chatterbox)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv) — torch/chatterbox/faster-whisper passen nicht ins
|
||||
# 3.14-Backend-venv. Bind 127.0.0.1: nur lokal; MC2 proxyt nach außen (routers/voice.py).
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/voice_service
|
||||
Environment=VOICE_PORT=8650
|
||||
Environment=VOICE_STT_MODEL=medium
|
||||
Environment=VOICE_STT_LANG=de
|
||||
Environment=VOICE_PIPER_DIR=%h/.voice/voices
|
||||
Environment=VOICE_PIPER_DEFAULT=de_DE-thorsten-medium
|
||||
Environment=VOICE_CHATTERBOX_DEVICE=cpu
|
||||
Environment=VOICE_CHATTERBOX_LANG=de
|
||||
Environment=TOKENIZERS_PARALLELISM=false
|
||||
ExecStart=%h/.voice/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8650
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
||||
#
|
||||
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
||||
# (Qwen3-Embedding-0.6B, für Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||
# (Qwen3-Embedding-0.6B). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
||||
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||
# anstoßen, sonst bleibt es kalt.
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# Auftragsbuch — die Vorschlags-Inbox der Box
|
||||
|
||||
Das Auftragsbuch ist das Mensch-Gate des propose-only-Kreislaufs als Klick statt
|
||||
Git-Handarbeit: alles, was die Box von allein baut oder sich ausdenkt, landet hier
|
||||
als Karte — du entscheidest mit **Annehmen** oder **Ablehnen**.
|
||||
|
||||
## Die Ideen-Queue davor (natives Hermes-Kanban, seit 10.07.2026)
|
||||
|
||||
Vor dem Auftragsbuch liegt DIE eine Ideen-Queue: das native Hermes-Kanban
|
||||
(`hermes kanban`, SQLite-Board, Dispatcher läuft im Gateway). Der Weg einer Idee:
|
||||
|
||||
1. **Idee rein — Tür egal:** Eingabefeld im Auftragsbuch-Tab (`POST /api/ideen`),
|
||||
Telegram/Desktop (natives Tool `kanban_create`, Regel in SOUL.md) oder Lucys
|
||||
Sprech-Leitung (`idee_notieren` in `mcp/mcp_voice.py`). Alles landet als
|
||||
`triage`-Aufgabe auf demselben Board.
|
||||
2. **Die Box arbeitet sie aus:** der Kanban-Specifier macht aus der rohen Idee
|
||||
einen sauberen Auftrag und wählt das Worker-Profil (Routing über die
|
||||
Profil-Beschreibungen: Code → `werkstatt` [Qwen3-Coder-Next, Leitplanken in
|
||||
`deploy/werkstatt-SOUL.md`], Doku/Analyse → `default`).
|
||||
3. **Der Worker baut** im isolierten Workspace; Code-Ergebnisse pusht er als
|
||||
Vorschlags-Branch auf Gitea — der hier als Karte erscheint. **Das Gate bleibt
|
||||
dein Klick.**
|
||||
|
||||
**Die Queue füttert sich auch selbst — Idle-Radar (S4 „Zündung", 12.07.2026):** ein
|
||||
nächtlicher Hermes-Cron (03:45, `deploy/idle-radar-feed.sh`) destilliert aus den
|
||||
Journal-Fehlermustern der letzten 24 h und der jüngsten Traum-Notiz bis zu **2 belegte**
|
||||
Wartungs-Kandidaten und legt sie als rohe Ideen (`triage`, `created_by=idle-radar`) auf
|
||||
dasselbe Board. Leitplanken: jedes Beleg-Zitat wird **mechanisch** gegen das Material
|
||||
geprüft (erfundene Belege fliegen raus — reproduziert-oder-abgelehnt), **Stau-Bremse**
|
||||
(ab 4 offenen Queue-Aufgaben oder 3 offenen Karten legt er nichts nach), dreifacher
|
||||
Dedup (State-Datei `~/.hermes/state/idle-radar-gemeldet.txt`, Board-Abgleich inklusive
|
||||
archivierter = verworfener Ideen, `--idempotency-key`), Security/Config/Updates tabu.
|
||||
Der Radar legt nur Ideen an — gebaut wird über denselben Kreislauf, dein Klick bleibt
|
||||
das Gate.
|
||||
|
||||
Die Queue-Sicht im Tab (Status, offene Ideen) kommt aus `hermes kanban list --json`
|
||||
(`backend/services/ideen.py`); die Morgenlage bekommt `kanban stats` als
|
||||
Beweismaterial. Entscheid nativ-vs-Eigenbau (Hermes-first): Probe 10.07.2026 —
|
||||
Dispatcher, Specifier, Profil-Modellwahl und Artefakt-Tracking sind nativ E2E
|
||||
bewiesen; ein Eigenbau hätte all das nur nachgebaut.
|
||||
|
||||
## Woher die Karten kommen
|
||||
|
||||
- **Fertige Patches** — Branches auf Gitea (`wartung/*`, `orchestrator/*`, `doku/*`,
|
||||
`feature/*`), wie die Werkstatt und der Orchestrator sie hinterlassen. Die Karte
|
||||
zeigt Commit-Botschaft, geänderte Dateien und den vollen Diff. Seit S3
|
||||
(lucy-pipeline) aus **zwei Repos**: `mission-control-v2` (Box-Stack) und `lucy`
|
||||
(Desktop-App, rosa „Lucy"-Badge auf der Karte).
|
||||
- **Skill-Kandidaten** — Ideen des nächtlichen Traum-Crons aus dem Wissens-Vault
|
||||
(`~/wissens-vault/skill-kandidaten/`). „Beauftragen" schickt sie als Auftrag an
|
||||
die Werkstatt (Orchestrator-Skill); das Ergebnis kommt als neuer Patch zurück.
|
||||
|
||||
## Was „Annehmen" bei einem Patch macht (deploy/auftrag-annehmen.sh)
|
||||
|
||||
1. Merge des Branches in einem **isolierten Worktree** (Live-Checkout bleibt unberührt).
|
||||
Kollidiert der Merge, weil main weitergelaufen ist, versucht der Runner **automatisch
|
||||
einen Rebase** des Branches auf main (Selbstheilung für veraltete Vorschläge) — erst
|
||||
wenn auch der kollidiert, fällt die Karte mit ehrlicher Meldung durch.
|
||||
2. `py_compile`-Gate über die geänderten Python-Dateien.
|
||||
3. Push nach `main` auf Gitea (mit Retry).
|
||||
4. Deploy (als /tmp-Kopie von deploy.sh — Selbst-Reset-Falle) — die Zentrale
|
||||
startet dabei kurz neu.
|
||||
5. Health-Check mit Geduld. **Rot ⇒ automatischer Revert des Merges + Redeploy +
|
||||
Alarm.** Grün ⇒ Branch wird aufgeräumt, Meldung an Telegram + Chronik.
|
||||
|
||||
Der Lauf ist eine eigene systemd-Unit (detached) — er überlebt den Neustart des
|
||||
Backends, das ihn gestartet hat. Fortschritt steht live auf der Karte
|
||||
(`/srv/models/mc2-auftragsbuch.json`).
|
||||
|
||||
**Kaputt aufgesetzte Branches** (Worker machte `git init`/Shallow statt zu klonen → kein
|
||||
gemeinsamer Ursprung mit main, „refusing to merge unrelated histories") erkennt das
|
||||
Auftragsbuch an der Quelle: rote Markierung „kein gemeinsamer Ursprung", der
|
||||
Annehmen-Knopf fehlt (Annehmen wäre ein garantierter Fehllauf), API und Runner halten
|
||||
zusätzlich dicht. Ebenso markiert: „Diff leer — bringt nichts" (Branch würde gegenüber
|
||||
main nichts ändern). In beiden Fällen: **ablehnen mit Grund** und die Idee frisch in die
|
||||
Queue geben; die Prävention (voll klonen, merge-base-Selbstcheck, fetch+rebase vor dem
|
||||
Push) steht in der werkstatt-SOUL.
|
||||
|
||||
## Was „Annehmen" bei einer LUCY-Karte macht (deploy/lucy-annahme.sh)
|
||||
|
||||
Lucy (Electron) lebt auf dem Windows-PC; ihr `dist` liegt nicht im Repo — die Box
|
||||
kann sie nicht bauen. Der Lucy-Runner spannt darum den PC ein (PC-Executor :7777,
|
||||
Token aus `~/.hermes/config.yaml`):
|
||||
|
||||
1. **Vorprüfung am PC, VOR dem Merge:** Executor erreichbar? Arbeitskopie
|
||||
`F:\Coding Stuff\lucy` auf `main` und sauber? Läuft Lucy gerade? Scheitert etwas,
|
||||
bricht der Lauf ab, ohne irgendetwas zu verändern — der PC muss an sein.
|
||||
2. Merge des Branches im isolierten Worktree von `~/lucy` → Push nach `main`.
|
||||
3. Der PC zieht `main` (ff-only) und startet **detached**
|
||||
`deploy/lucy-annahme.ps1` (liegt im Lucy-Repo): ggf. `npm ci` (nur wenn sich
|
||||
das Lockfile geändert hat) → `tsc --noEmit`-Gate → dist-Backup → `npm run dist`
|
||||
→ Lucy-Neustart **nur, wenn sie vorher lief** (User-Entscheid: Lucy startet
|
||||
nur, wenn der Commander sie will) → Stimme-Health (`:8130`).
|
||||
4. Die Box pollt die PC-Status-Datei (`.lucy-annahme.json`) und spiegelt den
|
||||
Fortschritt live auf die Karte. **Rot ⇒ Merge wird automatisch revertiert**,
|
||||
der PC stellt dist aus dem Backup wieder her — die laufende Lucy bleibt die
|
||||
alte. Grün ⇒ Branch aufgeräumt, Meldung an Telegram + Chronik.
|
||||
|
||||
## Bagatellen ohne Klick (mc2-bagatell.timer, 04:10)
|
||||
|
||||
Einzige Ausnahme vom Klick-Gate (User-Entscheid 10.07.2026, bewusst eng geschnitten):
|
||||
`deploy/bagatell-annahme.sh` spielt nachts Vorschlags-Branches ein, deren Diff
|
||||
**ausschließlich `.md`-Dateien anlegt oder ändert** — keine Code-Zeile, keine
|
||||
Löschungen, keine Renames, keine Skripte. Der Weg ist derselbe Annahme-Runner wie
|
||||
beim Klick (Merge → Gate → Deploy → Health → Auto-Revert), maximal 2 pro Nacht und
|
||||
nur Branches ohne vorherige Fehl-Läufe. Fangnetz: Nacht-Sicherung 03:30 liegt davor,
|
||||
jede Annahme steht in Chronik + Telegram, die Morgenlage 04:30 berichtet. Die Klasse
|
||||
erweitert nur der Commander, nie das Skript.
|
||||
|
||||
## „Lucy kennt sich" (12.07.2026): Selbstbild, Empfehlungs-Stempel, Lern-Gedächtnis
|
||||
|
||||
- **Selbst-Inventur (Cron 03:35, `deploy/selbst-inventur.sh`):** erhebt den
|
||||
**Selbst-Steckbrief** der Box live aus der Realität (Versionen, Dienste, Modelle,
|
||||
Crons, Queue, Karten → `~/.hermes/state/selbst-steckbrief.md`) und **bemerkt selbst,
|
||||
wenn sich etwas ändert** (Diff gegen gestern → stille Chronik-Karte + kurze
|
||||
Telegram-Zeile; keine Änderung = still). Der Steckbrief ist das „was es schon
|
||||
gibt"-Gedächtnis: Idle-Radar-Analyst und Werkstatt (Realitäts-Check-Regel) lesen ihn,
|
||||
bevor sie vorschlagen oder bauen.
|
||||
- **Karten-Gutachter (Cron 04:00, `deploy/karten-gutachter.sh`):** stempelt jede neue
|
||||
Karte mit **„Empfehlung: Annehmen/Ablehnen/Unklar" + einem ehrlichen Satz** —
|
||||
geprüft werden Redundanz (existiert das laut Steckbrief schon?), Risiko und Nutzen.
|
||||
Der Stempel ist eine Meinung, kein Gate; ein nachgeschobener Commit macht ihn
|
||||
ungültig (wird neu gestempelt). Ablage `/srv/models/mc2-karten-gutachten.json`.
|
||||
- **Ablehnen mit Grund:** Beim Ablehnen einer Karte kannst du der Box in einem Satz
|
||||
sagen, *warum* („gibt es schon", „will ich nicht"). Der Grund landet in
|
||||
`/srv/models/mc2-ablehnungen.jsonl` und wird Radar/Analysten als „NIE wieder
|
||||
vorschlagen"-Material vorgelegt — dein Klick wird damit zum Lehrer, nicht nur zum Tor.
|
||||
- **Radar-Treffer → Queue:** Meldet der wöchentliche Release-Radar einen echten Treffer,
|
||||
legt er zusätzlich EINE rohe Idee in die Ideen-Queue („Probe bauen?", idempotent je
|
||||
Release-Tag) — der Blick nach draußen füttert damit denselben Kreislauf.
|
||||
|
||||
## Curator Auto-Archivierung (Cron `0 3 * * *` — täglich um 03:00)
|
||||
|
||||
**Zweck:** Automatische Archivierung inaktiver Skills nach 14 Tagen ohne Nutzung.
|
||||
|
||||
**Skripte:**
|
||||
- `~/.hermes/scripts/curator-archive-watchdog.sh` — Cron-Einstieg, ruft das Python-Skript auf.
|
||||
- `~/.hermes/scripts/curator_archive_logic.py` — Hauptlogik. Liest `.curator_state` und `.usage.json` aus `~/.hermes/skills/`, prüft auf Inaktivität (>14 Tage seit letztem Gebrauch), überspringt gepinnte und exkludierte Skills, verschiebt Skills nach `.archive/<Kategorie>/`.
|
||||
- `~/.hermes/scripts/curator-idle-watchdog.sh` — Zusätzlicher Watchdog (Sonntag 00:00, Cron `0 0 * * 0`): prüft, ob der Curator selbst ≥14 Tage idle ist und triggert ggf. eine Reaktivierung.
|
||||
|
||||
**Konfiguration** (`~/.hermes/skills/.curator_state`):
|
||||
- `archivierung.enabled: false` — Archivierung muss explizit aktiviert werden.
|
||||
- `archivierung.threshold_days: 14` — Tage ohne Nutzung bis zur Archivierung.
|
||||
- `archivierung.excluded_skills: [...]` — Liste der nie zu archivierenden Skills.
|
||||
- `archivierung.last_archive_run_at` — Zeitstempel des letzten Durchlaufs.
|
||||
|
||||
**Ablauf im Durchlauf:**
|
||||
1. Prüft, ob Archivierung aktiviert ist.
|
||||
2. Läuft alle Skills durch `.usage.json`.
|
||||
3. Überspringt: gepinnte Skills, exkludierte Skills, aktive Skills (<14 Tage).
|
||||
4. Archiviert: State auf "archived" setzen, Verzeichnis nach `.archive/<Kategorie>/` verschieben, Usage-Datei aktualisieren.
|
||||
5. Protokolliert in `~/.hermes/logs/curator/archive-<Datum>.log`.
|
||||
|
||||
**Sicherheit:**
|
||||
- Archivierung standardmäßig deaktiviert — passiert nie ohne explizite Freigabe.
|
||||
- Gepinnte und exkludierte Skills werden nie berührt.
|
||||
- Archivierung ist reversibel: Skills können manuell aus `.archive/` zurückverschoben werden.
|
||||
|
||||
## Leitplanken
|
||||
|
||||
- Propose-only bleibt: **keine Code-Zeile geht ohne Klick live.** Das Gate ist dieses
|
||||
Buch; einzige Ausnahme sind die eng geschnittenen Doku-Bagatellen (oben).
|
||||
- „Frontend ohne Build"-Warnung (nur mc2): enthält ein Branch `frontend/src` ohne
|
||||
frisches `frontend/dist`, bleibt die Oberfläche nach dem Annehmen alt, bis am PC
|
||||
gebaut wird. Lucy-Karten brauchen die Warnung nicht — dort baut IMMER der PC.
|
||||
- Ablehnen löscht nur den Remote-Branch; die Historie auf Gitea bleibt.
|
||||
|
||||
Diese Datei kam übrigens selbst über das Auftragsbuch ins Repo — als erste echte
|
||||
End-to-End-Annahme (Probe-Branch `wartung/annahme-probe`, 08.07.2026).
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
## Die Bereiche (Sidebar, Stand 09.07.2026)
|
||||
- **Cockpit** — deine Box auf einen Blick (Status, Leistung, Morgenlage, Erinnerungen).
|
||||
- **Auftragsbuch** — Vorschläge der Box (Werkstatt/Orchestrator/Traum) per Ein-Klick annehmen/ablehnen.
|
||||
- **Modelle** — Speicherleiste, installierte Modelle + neue finden/laden, Rollen (hermes/fast/heavy/…).
|
||||
- **Gedächtnis** — geteilte Fakten/Regeln, die ALLE Tools (Hermes, Desktop, PC) via MCP lesen/schreiben.
|
||||
- **Wissen** — Lucys Wissens-Vault (Traum-Notizen, `[[verlinkt]]` navigierbar).
|
||||
|
||||
+325
-328
@@ -1,328 +1,325 @@
|
||||
# Konzept: Bare-Metal-Wiederaufbau + First-Run-Wizard (VOLLSTÄNDIG)
|
||||
|
||||
> **Zweck:** Plan für den „hard crash"-Fall — die Box (`tobisniceaiarbeitstier`, Ubuntu 26.04,
|
||||
> AMD Ryzen AI MAX+ 395 / gfx1151, 122 GB RAM) muss von **null** wiederherstellbar sein.
|
||||
> **Anspruch:** *ALLES* ist erfasst — Engine, Hermes-Konfig 1:1, 3D-Avatare, Voice/Klonstimme,
|
||||
> Memory, Browser, MCP, Skills, Secrets. Pro Komponente entscheidet der Nutzer im Wizard:
|
||||
> **1:1 zurück** · **neu/Default** · **weglassen**.
|
||||
>
|
||||
> Dieses Dokument ist die **Spezifikation für eine künftige Implementierungs-Session** — es baut
|
||||
> noch nichts. Stand: 2026-06-28.
|
||||
|
||||
---
|
||||
|
||||
## 1. Leitprinzip: 1:1 ODER modular — der Nutzer wählt
|
||||
|
||||
Der Wizard behandelt jede Komponente als **eigene Kachel mit drei Modi**:
|
||||
|
||||
| Modus | Bedeutung |
|
||||
|---|---|
|
||||
| 📦 **1:1 aus Backup** | Exakter alter Zustand wird zurückgespielt (Configs, Daten, Klonstimme, Avatare). |
|
||||
| 🆕 **Neu / Default** | Frische Installation mit sinnvollen Defaults (z.B. entfesseltes Hermes-Profil, Default-Avatar). |
|
||||
| ⏭️ **Weglassen** | Komponente wird (vorerst) nicht installiert. |
|
||||
|
||||
Ein „Alles 1:1"-Knopf wählt überall 📦 (wo ein Backup existiert), sonst 🆕. So bekommt der Nutzer
|
||||
entweder den exakten alten Stand oder kann gezielt entrümpeln.
|
||||
|
||||
---
|
||||
|
||||
## 2. Was es schon gibt (wiederverwenden)
|
||||
|
||||
| Asset | Datei | Deckt ab |
|
||||
|---|---|---|
|
||||
| Code-Deploy | `deploy/deploy.sh` | git pull + venvs + Units + Restart. **Setzt Engine/llama-swap/Dirs/venvs voraus.** |
|
||||
| Zustands-Backup | `deploy/backup.sh` + `mc2-backup.{service,timer}` | **Aktuell** (live): mem0, `~/.hermes/{config.yaml,.env,plugins}`, llama-swap-config. Retention 14. **Für echtes 1:1 zu erweitern** — fertiges Snippet in **Anhang B** (§5). |
|
||||
| Restore | `deploy/restore.sh` | Spielt Tarball zurück (mit Pre-Restore-Sicherung). |
|
||||
| Engine | `deploy/provision-engine.sh` | llama.cpp Vulkan/RADV-Build (root). |
|
||||
| Voice-Setup | `voice_service/install.sh` | venv + Piper-Binary + dt. Piper-Stimmen + Chatterbox (best-effort). |
|
||||
| Updates | `deploy/update-engine.sh`, `update-swap.sh` | Laufende Engine/Router-Updates. |
|
||||
| Postchecks | `deploy/stack-postcheck.sh`, `hermes-postcheck.sh` | Funktionsprüfung → Wizard-Verifikationsschritt. |
|
||||
|
||||
---
|
||||
|
||||
## 3. VOLLSTÄNDIGER Komponenten-Katalog
|
||||
|
||||
> **Scan-verifiziert (2026-06-28)** gegen `backend/config.py`, alle `backend/services/*` & `routers/*`,
|
||||
> `frontend/src/{nav.ts,views,components/voice}`, `voice_service/app.py`, `mem0_service/`, `deploy/*`.
|
||||
> Alle persistenten Schreibpfade, Dienste, Secrets und Env-Vars sind unten erfasst.
|
||||
|
||||
Legende Restore-Quelle: 📦 = aus Backup-Tarball · ⬇️ = Re-Download/Install (Skript) · 🌐 = Git ·
|
||||
🔑 = Secret (Nutzer/Generieren) · 🆕 = im Wizard neu gewählt.
|
||||
„Im Backup?" = deckt der **aktuelle** `backup.sh` es ab.
|
||||
|
||||
### A · OS & System (L0, root/sudo, einmalig)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Ubuntu 26.04, User `hitonabi`, `enable-linger` | — | ⬇️ manuell | n/a |
|
||||
| System-Pakete: python3.14+venv, git, curl, jq, ttyd, uv | — | ⬇️ apt/curl | nein |
|
||||
| Vulkan-Stack: mesa-vulkan-drivers, libvulkan1, vulkan-tools | — | ⬇️ apt | nein |
|
||||
| Chrome-Libs (Browser): `agent-browser install --with-deps` | — | ⬇️ apt | nein |
|
||||
| Verzeichnis-Layout: `/srv/models/{,mem0,mc2-backups,drafts}`, `/etc/llama-swap/` | — | ⬇️ mkdir+chown | nein |
|
||||
|
||||
### B · Inferenz-Engine + Router (L1, root)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| llama.cpp (Vulkan) → `llama-server` | `/opt/llamacpp-vulkan`, `/usr/local/bin/llama-server` | ⬇️ provision-engine.sh / update-engine.sh (ggml-org Release) | nein (re-build) |
|
||||
| **llama-swap** Binary (Router `:8080`) | `/usr/local/bin/llama-swap` | ⬇️ **bekannt:** `mostlygeek/llama-swap`-Release (Rezept in `update-swap.sh`) | nein (re-download) |
|
||||
| **llama-swap systemd-System-Unit** | `/etc/systemd/system/llama-swap.service` (+ `.d/` Drop-ins) | ⬇️ Bootstrap legt sie an — **kompletter Unit-Inhalt in Anhang A** (von der Box abgegriffen; Drop-ins via provision-engine.sh) | nein |
|
||||
| llama-swap-Config (Modelle/Rollen) | `/etc/llama-swap/config.yaml` | 📦 | **ja** |
|
||||
| Draft-Modelle (Spec-Decoding) | `/srv/models/drafts/` | ⬇️ Re-Download | nein |
|
||||
|
||||
### C · MC2-App (L2, userspace)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| MC2-Code | `~/mission-control-v2` | 🌐 Git (Gitea) | n/a |
|
||||
| Backend-venv (Python 3.14) | `backend/.venv` | ⬇️ deploy.sh | nein (rebuild) |
|
||||
| Frontend (gebaut, inkl. **3D-Avatar `avatar.vrm`**) | `frontend/dist`, `frontend/public/avatar.vrm` | 🌐 Git | n/a |
|
||||
| systemd-User-Dienst `mission-control-2` (`:9001`) | `~/.config/systemd/user/` | ⬇️ deploy.sh | nein |
|
||||
|
||||
### D · 3D-Avatar (Sprechen-Tab)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Default-Avatar (VRM) | `frontend/public/avatar.vrm` (→ dist) | 🌐 Git | n/a |
|
||||
| Renderer | `frontend/src/components/voice/Avatar3D.tsx` | 🌐 Git | n/a |
|
||||
| **Eigene/zusätzliche Avatare** (falls Nutzer welche ablegt) | **TODO: Ablageort definieren** (z.B. `/srv/models/avatars/` + DB-Verweis) | 📦/🆕 | **nein (Lücke)** |
|
||||
|
||||
> Heute ist der Avatar **fest** (`avatar.vrm`, kommt mit dem Git-Frontend zurück). Wenn künftig
|
||||
> Nutzer-Avatare hochgeladen werden, brauchen sie einen persistenten Ablageort, der ins Backup geht.
|
||||
|
||||
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Voice-venv (Python 3.12) | `~/.voice/venv` | ⬇️ install.sh | nein (rebuild) |
|
||||
| STT (faster-whisper Modell) | `~/.voice/` (Cache) | ⬇️ install.sh / 1. Start | nein |
|
||||
| Piper-Binary + dt. Stimmen (thorsten/kerstin) | `~/.voice/piper`, `~/.voice/voices` | ⬇️ install.sh | nein |
|
||||
| Chatterbox (Premium-TTS, CPU-torch) | venv | ⬇️ install.sh (best-effort) | nein |
|
||||
| **Klonstimme / Voice-Referenz-Audio** (Nutzer) | `~/.voice/refs/ref.wav` (`VOICE_REFS_DIR`, `/api/voice/reference`) | 📦 | **nein → Anhang B** |
|
||||
| ElevenLabs-Key | `~/.hermes/.env` | 🔑 | ja |
|
||||
| Stimm-/Lautstärke-Wahl | Browser-`localStorage` (pro Gerät) | 🆕 client | n/a |
|
||||
| Dienst `voice-service` (`:8650`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
|
||||
### F · Mem0 (Langzeitgedächtnis)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Mem0-venv (Python 3.12, uv) | `~/.mem0/venv` | ⬇️ deploy.sh | nein (rebuild) |
|
||||
| **Gedächtnis-Daten (Chroma + history.db)** | `/srv/models/mem0/` | 📦 | **ja** |
|
||||
| Hermes-Memory-Plugin | `~/.hermes/plugins/mc2-memory/` | 📦/🌐 | ja |
|
||||
| Dienst `mem0-service` (`:8765`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
|
||||
### G · Hermes-Agent (das Herz)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Hermes-Code | `~/.hermes/hermes-agent` | 🌐 Git (NousResearch) | nein (re-clone) |
|
||||
| Bundled Node v22 | `~/.hermes/node/bin` | ⬇️ (kommt mit Hermes) | nein |
|
||||
| Hermes-venv | `~/.hermes/hermes-agent/venv` | ⬇️ rebuild | nein |
|
||||
| **`config.yaml` 1:1** (Toolsets, MCP, Engine, Browser, Personalities, alles) | `~/.hermes/config.yaml` | 📦 | **ja** |
|
||||
| **`.env` (Secrets: TELEGRAM_BOT_TOKEN, API_SERVER_KEY, ElevenLabs …)** | `~/.hermes/.env` | 📦/🔑 | **ja** |
|
||||
| Plugins | `~/.hermes/plugins/` | 📦 | ja |
|
||||
| **Skills (eigene)** | `~/.hermes/skills/` (45M; `.hub`-Cache re-downloadbar) | 📦 | **nein → Anhang B (ohne .hub)** |
|
||||
| Sessions/History (optional) | `~/.hermes/sessions/` (856K) | 📦 | **nein → Anhang B** |
|
||||
| Checkpoints (optional) | `~/.hermes/checkpoints/` (existiert nicht) | ⏭️ skip | nein |
|
||||
| **Token-/Ersparnis-Statistik** | `~/.hermes/token_stats.json` | 📦 | **nein → Anhang B** |
|
||||
| Dienst `hermes-gateway` (`:8642`) | systemd-User | ⬇️ | nein |
|
||||
| Desktop-Gateway `hermes-builtin-ui` (`:9119`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
| Desktop-Gateway-Token-Drop-in | `~/.config/systemd/user/hermes-builtin-ui.service.d/session-token.conf` | 🔑 neu erzeugen (Wert auch in `~/.hermes/desktop-gateway-token`, chmod 600) | **nein** |
|
||||
| Desktop-Gateway-Token-Kopie | `~/.hermes/desktop-gateway-token` (chmod 600) | 🔑 | **nein** |
|
||||
|
||||
### H · MCP-Server (4)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| `mission-control-memory`, `-stack` (MC2-venv) | `~/mission-control-v2/mcp/*.py` | 🌐 Git | über config.yaml |
|
||||
| `hermes-pc-control`, `hermes-web-fetch` (Hermes-venv) | dito | 🌐 Git | über config.yaml |
|
||||
| MCP-Verdrahtung | `~/.hermes/config.yaml → mcp_servers` | 📦 | ja |
|
||||
|
||||
### I · Browser-Stack
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| agent-browser (npm global) | `~/.local/...`, symlink `~/.hermes/node/bin` | ⬇️ npm i -g | nein |
|
||||
| Chrome (engine) + System-Libs | `~/.agent-browser/browsers/` + apt-Libs | ⬇️ install --with-deps | nein |
|
||||
| lightpanda (optional, leicht) | `~/.local/bin/lightpanda` | ⬇️ Download | nein |
|
||||
| Browser-Verdrahtung (engine=chrome, toolset) | `~/.hermes/config.yaml` | 📦 | ja |
|
||||
|
||||
### J · Modelle (GGUF — der große Brocken)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| GGUF-Modelle (Brain, fast, heavy, coder, vision, embedding …) | `/srv/models/` | ⬇️ Re-Download aus llama-swap-Manifest | **nein (zu groß, bewusst)** |
|
||||
|
||||
### K · Extern (anderer Rechner)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| PC-Executor (Windows) | `client/hermes-pc/` → Scheduled Task | 🌐 Git + Task | n/a (anderer Host) |
|
||||
| Hermes Desktop (Windows-App) | `%LOCALAPPDATA%\hermes` + `connection.json` in `%APPDATA%\Hermes\` | ⬇️ Hermes-Setup.exe + Remote-URL/Token neu eintragen | n/a (anderer Host) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Ziel-Architektur
|
||||
|
||||
### 4.1 `deploy/bootstrap.sh` — orchestrierter From-Zero-Lauf
|
||||
- **Idempotent & resumierbar** (Schritt-Marker in `~/.mc2-bootstrap.state`).
|
||||
- **Getrennt nach sudo-Bedarf**: `bootstrap-root.sh` (L0+L1, bewusst mit sudo) + `bootstrap.sh`
|
||||
(L2+, sudo-frei = Kern ist `deploy.sh`). Passt zum Nordstern „Runtime ohne sudo".
|
||||
- **Mündet in den Wizard**: startet MC2 im First-Run-Modus, gibt Wizard-URL aus.
|
||||
|
||||
Phasen: `0 Vorflug → 1[sudo] System+Dirs → 2[sudo] Engine+llama-swap → 3 Code(MC2+Hermes+Node)
|
||||
→ 4 venvs(backend/mem0/voice/hermes) → 5 Browser → 6 deploy.sh(Units/enable/restart)
|
||||
→ 7 Restore(optional) → 8 Wizard hoch + postcheck`.
|
||||
|
||||
### 4.2 First-Run-Wizard (browserbasiert, von MC2 serviert)
|
||||
MC2 erkennt unkonfigurierten Zustand → Frontend-Route `/setup` statt Dashboard. Schritte:
|
||||
|
||||
1. **Systemcheck** — Live-Ampel je Komponente aus §3 (`GET /api/setup/status` + `…/system/services`).
|
||||
2. **Restore-Quelle wählen** — Backup-Tarball erkennen → globaler Modus „Alles 1:1" / „selektiv" / „frisch".
|
||||
3. **Komponenten-Auswahl (Kernstück)** — pro Katalog-Eintrag aus §3 eine Kachel mit
|
||||
📦/🆕/⏭️ (siehe §1). Zeigt Größe + ob Backup-Daten vorhanden.
|
||||
4. **Netzwerk & Identität** — Box-IP (→ `HERMES_TERMINAL_URL`, `PC_EXECUTOR_URL`), Hostname.
|
||||
5. **Secrets** 🔑 — `TELEGRAM_BOT_TOKEN`, erlaubte User-ID, `API_SERVER_KEY` (oder generieren),
|
||||
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
||||
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
||||
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
||||
7. **Avatar & Voice** — Avatar wählen (Default-VRM oder eigener), Stimme/Klonstimme
|
||||
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
||||
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
||||
Fortschritt `GET /api/jobs`) ODER geführte Discover-Neuauswahl. Reihenfolge: Brain → heavy → Rest.
|
||||
9. **Externe Checkliste** — PC-Executor (Windows-Task), Hermes Desktop (App + Token) als Haken.
|
||||
10. **Verifikation & Abschluss** — `stack-postcheck.sh` + `hermes-postcheck.sh` → grün/rot-Liste → Dashboard.
|
||||
|
||||
**Technik:** neuer Router `backend/routers/setup.py`
|
||||
(`GET /api/setup/status`, `POST /api/setup/{secrets,network,hermes,components,finish}`).
|
||||
First-Run-Gate: MC2 prüft beim Boot Marker `~/.mc2-setup-done` bzw. Pflicht-Secrets → leitet auf `/setup`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backup-Scope für echtes 1:1 ERWEITERN (umzusetzen — Snippet in Anhang B)
|
||||
|
||||
Der **aktuelle** `backup.sh` (live auf der Box, unverändert) reicht für „1:1" nicht. Für die Bau-Session
|
||||
liegt das **fertige Erweiterungs-Snippet in Anhang B** — es ergänzt den Tarball um:
|
||||
|
||||
- **Voice-Klonstimme** → `~/.voice/refs/` (`ref.wav`)
|
||||
- **Token-/Ersparnis-Statistik** → `~/.hermes/token_stats.json`
|
||||
- **Hermes-Skills** → `~/.hermes/skills/` (re-downloadbarer `.hub`-Cache per `tar --exclude` ausgelassen)
|
||||
- **Hermes-Sessions/History** → `~/.hermes/sessions/`
|
||||
|
||||
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
||||
mit neu starten.
|
||||
|
||||
**Offen bleibt:** eigene Avatare (sobald Upload existiert — Ablageort heute undefiniert, siehe §3·D).
|
||||
|
||||
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
||||
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
||||
|
||||
→ Aufgabe der Bau-Session: `backup.sh` + `restore.sh` um diese Pfade erweitern (mit klarer
|
||||
„opt-in für große/optionale Teile"-Logik), und den MANIFEST-Inhalt entsprechend.
|
||||
|
||||
---
|
||||
|
||||
## 6. Secrets-Strategie
|
||||
- Single Source: `~/.hermes/.env` (chmod 600), im Tarball (selbst 600).
|
||||
- Rebuild ohne Backup → Wizard-Schritt 5 erzeugt sie. `API_SERVER_KEY` generierbar; Telegram/ElevenLabs liefert Nutzer.
|
||||
- Nie ins Git, nie in Logs, nie in die MC2-DB. **Off-Box-Spiegelung des Tarballs noch offen** ([[mc2-backup-restore]]).
|
||||
|
||||
## 7. Modell-Strategie
|
||||
- **A (empfohlen):** `/etc/llama-swap/config.yaml` (im Backup) listet alle Modelle → Wizard leitet Repos/Quants ab und lädt automatisch (`/api/models/install`). „Ein Klick, lädt über Nacht."
|
||||
- **B:** geführte Discover-Neuauswahl je Rolle. Reihenfolge Brain → heavy → Rest, danach `warmup.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementierungs-Reihenfolge (neue Session)
|
||||
1. ✅ **Box-Verifikation erledigt (2026-06-28, nur gelesen — nichts verändert):** llama-swap.service-Inhalt
|
||||
in **Anhang A**; skills/sessions/refs/token_stats existieren (Pfade in §3 verifiziert); Backup-Erweiterung
|
||||
als fertiges Snippet in **Anhang B**. **Keine offenen Box-Fragen mehr** außer §10·3–6. → mit Schritt 2 starten.
|
||||
2. `backup.sh`/`restore.sh` um die 1:1-Lücken erweitern (§5).
|
||||
3. `deploy/bootstrap.sh` + `bootstrap-root.sh` (Phasen, State-Marker); llama-swap-Install skripten.
|
||||
4. `backend/routers/setup.py` + First-Run-Gate.
|
||||
5. Frontend `/setup`-Wizard (Schritte 1–10), Komponenten-Auswahl als Kernstück; bestehende Views
|
||||
(Cockpit/AgentView/Sprechen) wiederverwenden.
|
||||
6. Modell-Restore-aus-Manifest.
|
||||
7. **Abnahme in frischer VM** (§9).
|
||||
|
||||
Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
||||
|
||||
## 9. Abnahmekriterien
|
||||
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
||||
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), 3D-Avatar, Browser funktionieren.
|
||||
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
||||
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
||||
- Idempotenz: zweiter Lauf = No-op.
|
||||
|
||||
## 10. Offene Entscheidungen (vor dem Bau)
|
||||
1. ~~llama-swap systemd-Unit-Inhalt~~ → **geklärt: kompletter Unit in Anhang A** (in der Bau-Session anlegen).
|
||||
2. ~~Voice-Referenz-Audio-Ort~~ → **geklärt: `~/.voice/refs/`** (Backup-Snippet in Anhang B, in der Bau-Session umsetzen).
|
||||
3. **Eigene Avatare**: künftiger Upload-/Ablage-Mechanismus + Backup-Pfad (einziger offener 1:1-Daten-Punkt).
|
||||
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
||||
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
||||
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
||||
|
||||
## 11. Referenzen
|
||||
- `backend/config.py`, [[project-mc2-architecture]], [[project-stack-state]]
|
||||
- [[project-hermes-setup]], `docs/HERMES_SETUP.md` (entfesseltes Profil, Browser, PC-Executor)
|
||||
- [[voice-sprechen-feature]] (Voice + 3D-Avatar), [[mem0-memory-architecture]], [[mc2-backup-restore]]
|
||||
- `deploy/{deploy,backup,restore,provision-engine,stack-postcheck,warmup}.sh`, `voice_service/install.sh`
|
||||
|
||||
---
|
||||
|
||||
## Anhang A — `llama-swap.service` (1:1 von der Box abgegriffen, 2026-06-28)
|
||||
|
||||
Base-Unit nach `/etc/systemd/system/llama-swap.service` (root). User/GFX sind boxspezifisch
|
||||
(hitonabi, gfx1151 → HSA 11.5.1) — auf anderer Hardware anpassen. Die `.d/`-Drop-ins
|
||||
(`vulkan.conf`, `warmup.conf`) legt `provision-engine.sh` an.
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=llama-swap (lokaler LLM Router)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=hitonabi
|
||||
Environment=HSA_OVERRIDE_GFX_VERSION=11.5.1
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/usr/local/bin/llama-swap --config /etc/llama-swap/config.yaml --listen 0.0.0.0:8080 --watch-config
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
```ini
|
||||
# /etc/systemd/system/llama-swap.service.d/vulkan.conf
|
||||
[Service]
|
||||
Environment=LD_LIBRARY_PATH=/opt/llamacpp-vulkan
|
||||
```
|
||||
```ini
|
||||
# /etc/systemd/system/llama-swap.service.d/warmup.conf
|
||||
[Service]
|
||||
ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh
|
||||
```
|
||||
|
||||
**Erstinstall-Reihenfolge:** `bash deploy/update-swap.sh` (Binary) → Unit kopieren →
|
||||
`sudo bash deploy/provision-engine.sh` (Engine+Drop-ins) → `sudo systemctl enable --now llama-swap`.
|
||||
|
||||
---
|
||||
|
||||
## Anhang B — Backup-Scope-Erweiterung für echtes 1:1 (fertiges Snippet)
|
||||
|
||||
In `deploy/backup.sh` ergänzen (Variablen oben: `VOICE="${VOICE_HOME:-$HOME/.voice}"`,
|
||||
Stage zusätzlich `"$STAGE/voice"`):
|
||||
|
||||
```bash
|
||||
# 1:1-Erweiterung (scan-verifiziert 2026-06-28):
|
||||
[ -f "$HERMES/token_stats.json" ] && cp -a "$HERMES/token_stats.json" "$STAGE/hermes/" || true
|
||||
[ -d "$HERMES/skills" ] && cp -a "$HERMES/skills" "$STAGE/hermes/skills" || true
|
||||
[ -d "$HERMES/sessions" ] && cp -a "$HERMES/sessions" "$STAGE/hermes/sessions" || true
|
||||
[ -d "$VOICE/refs" ] && cp -a "$VOICE/refs" "$STAGE/voice/refs" || true
|
||||
```
|
||||
tar-Aufruf um den re-downloadbaren Skill-Cache erleichtern:
|
||||
```bash
|
||||
tar --exclude='./hermes/skills/.hub' -czf "$OUT" -C "$STAGE" .
|
||||
```
|
||||
In `deploy/restore.sh` spiegelbildlich (skills/sessions **mergen**, nicht ersetzen) und
|
||||
`voice-service` mit neustarten:
|
||||
```bash
|
||||
VOICE="${VOICE_HOME:-$HOME/.voice}"
|
||||
SERVICES="mem0-service voice-service mission-control-2 hermes-gateway"
|
||||
[ -f "$STAGE/hermes/token_stats.json" ] && cp -a "$STAGE/hermes/token_stats.json" "$HERMES/"
|
||||
[ -d "$STAGE/hermes/skills" ] && { mkdir -p "$HERMES/skills"; cp -a "$STAGE/hermes/skills/." "$HERMES/skills/"; }
|
||||
[ -d "$STAGE/hermes/sessions" ] && { mkdir -p "$HERMES/sessions"; cp -a "$STAGE/hermes/sessions/." "$HERMES/sessions/"; }
|
||||
[ -d "$STAGE/voice/refs" ] && { mkdir -p "$VOICE/refs"; cp -a "$STAGE/voice/refs/." "$VOICE/refs/"; }
|
||||
```
|
||||
# Konzept: Bare-Metal-Wiederaufbau + First-Run-Wizard (VOLLSTÄNDIG)
|
||||
|
||||
> **Zweck:** Plan für den „hard crash"-Fall — die Box (`tobisniceaiarbeitstier`, Ubuntu 26.04,
|
||||
> AMD Ryzen AI MAX+ 395 / gfx1151, 122 GB RAM) muss von **null** wiederherstellbar sein.
|
||||
> **Anspruch:** *ALLES* ist erfasst — Engine, Hermes-Konfig 1:1, Voice/Klonstimme,
|
||||
> Memory, Browser, MCP, Skills, Secrets. Pro Komponente entscheidet der Nutzer im Wizard:
|
||||
> **1:1 zurück** · **neu/Default** · **weglassen**.
|
||||
>
|
||||
> Dieses Dokument ist die **Spezifikation für eine künftige Implementierungs-Session** — es baut
|
||||
> noch nichts. Stand: 2026-06-28.
|
||||
|
||||
---
|
||||
|
||||
## 1. Leitprinzip: 1:1 ODER modular — der Nutzer wählt
|
||||
|
||||
Der Wizard behandelt jede Komponente als **eigene Kachel mit drei Modi**:
|
||||
|
||||
| Modus | Bedeutung |
|
||||
|---|---|
|
||||
| 📦 **1:1 aus Backup** | Exakter alter Zustand wird zurückgespielt (Configs, Daten, Klonstimme). |
|
||||
| 🆕 **Neu / Default** | Frische Installation mit sinnvollen Defaults (z.B. entfesseltes Hermes-Profil). |
|
||||
| ⏭️ **Weglassen** | Komponente wird (vorerst) nicht installiert. |
|
||||
|
||||
Ein „Alles 1:1"-Knopf wählt überall 📦 (wo ein Backup existiert), sonst 🆕. So bekommt der Nutzer
|
||||
entweder den exakten alten Stand oder kann gezielt entrümpeln.
|
||||
|
||||
---
|
||||
|
||||
## 2. Was es schon gibt (wiederverwenden)
|
||||
|
||||
| Asset | Datei | Deckt ab |
|
||||
|---|---|---|
|
||||
| Code-Deploy | `deploy/deploy.sh` | git pull + venvs + Units + Restart. **Setzt Engine/llama-swap/Dirs/venvs voraus.** |
|
||||
| Zustands-Backup | `deploy/backup.sh` + `mc2-backup.{service,timer}` | **Aktuell** (live): mem0, `~/.hermes/{config.yaml,.env,plugins}`, llama-swap-config. Retention 14. **Für echtes 1:1 zu erweitern** — fertiges Snippet in **Anhang B** (§5). |
|
||||
| Restore | `deploy/restore.sh` | Spielt Tarball zurück (mit Pre-Restore-Sicherung). |
|
||||
| Engine | `deploy/provision-engine.sh` | llama.cpp Vulkan/RADV-Build (root). |
|
||||
| Voice-Setup | `voice_service/install.sh` | venv + Piper-Binary + dt. Piper-Stimmen + Chatterbox (best-effort). |
|
||||
| Updates | `deploy/update-engine.sh`, `update-swap.sh` | Laufende Engine/Router-Updates. |
|
||||
| Postchecks | `deploy/stack-postcheck.sh`, `hermes-postcheck.sh` | Funktionsprüfung → Wizard-Verifikationsschritt. |
|
||||
|
||||
---
|
||||
|
||||
## 3. VOLLSTÄNDIGER Komponenten-Katalog
|
||||
|
||||
> **Scan-verifiziert (2026-06-28)** gegen `backend/config.py`, alle `backend/services/*` & `routers/*`,
|
||||
> `frontend/src/{nav.ts,views,components/voice}`, `voice_service/app.py`, `mem0_service/`, `deploy/*`.
|
||||
> Alle persistenten Schreibpfade, Dienste, Secrets und Env-Vars sind unten erfasst.
|
||||
|
||||
Legende Restore-Quelle: 📦 = aus Backup-Tarball · ⬇️ = Re-Download/Install (Skript) · 🌐 = Git ·
|
||||
🔑 = Secret (Nutzer/Generieren) · 🆕 = im Wizard neu gewählt.
|
||||
„Im Backup?" = deckt der **aktuelle** `backup.sh` es ab.
|
||||
|
||||
### A · OS & System (L0, root/sudo, einmalig)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Ubuntu 26.04, User `hitonabi`, `enable-linger` | — | ⬇️ manuell | n/a |
|
||||
| System-Pakete: python3.14+venv, git, curl, jq, ttyd, uv | — | ⬇️ apt/curl | nein |
|
||||
| Vulkan-Stack: mesa-vulkan-drivers, libvulkan1, vulkan-tools | — | ⬇️ apt | nein |
|
||||
| Chrome-Libs (Browser): `agent-browser install --with-deps` | — | ⬇️ apt | nein |
|
||||
| Verzeichnis-Layout: `/srv/models/{,mem0,mc2-backups,drafts}`, `/etc/llama-swap/` | — | ⬇️ mkdir+chown | nein |
|
||||
|
||||
### B · Inferenz-Engine + Router (L1, root)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| llama.cpp (Vulkan) → `llama-server` | `/opt/llamacpp-vulkan`, `/usr/local/bin/llama-server` | ⬇️ provision-engine.sh / update-engine.sh (ggml-org Release) | nein (re-build) |
|
||||
| **llama-swap** Binary (Router `:8080`) | `/usr/local/bin/llama-swap` | ⬇️ **bekannt:** `mostlygeek/llama-swap`-Release (Rezept in `update-swap.sh`) | nein (re-download) |
|
||||
| **llama-swap systemd-System-Unit** | `/etc/systemd/system/llama-swap.service` (+ `.d/` Drop-ins) | ⬇️ Bootstrap legt sie an — **kompletter Unit-Inhalt in Anhang A** (von der Box abgegriffen; Drop-ins via provision-engine.sh) | nein |
|
||||
| llama-swap-Config (Modelle/Rollen) | `/etc/llama-swap/config.yaml` | 📦 | **ja** |
|
||||
| Draft-Modelle (Spec-Decoding) | `/srv/models/drafts/` | ⬇️ Re-Download | nein |
|
||||
|
||||
### C · MC2-App (L2, userspace)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| MC2-Code | `~/mission-control-v2` | 🌐 Git (Gitea) | n/a |
|
||||
| Backend-venv (Python 3.14) | `backend/.venv` | ⬇️ deploy.sh | nein (rebuild) |
|
||||
| Frontend (gebaut) | `frontend/dist` | 🌐 Git | n/a |
|
||||
| systemd-User-Dienst `mission-control-2` (`:9001`) | `~/.config/systemd/user/` | ⬇️ deploy.sh | nein |
|
||||
|
||||
### D · 3D-Avatar — **ausgebaut (28.08.2026)**
|
||||
|
||||
MC2 hat keinen Avatar mehr. `frontend/public/avatar.vrm` (24,5 MB) lag im Auslieferungsordner,
|
||||
wurde aber von keiner Zeile des Frontends referenziert — der Renderer `Avatar3D.tsx` war schon
|
||||
vorher verschwunden. Beides ist beim v3-Umbau (Etappe P0) entfernt worden; damit fällt auch die
|
||||
`.gitignore`-Sonderregel und der Direkt-Deploy-Schritt weg. **Nichts wiederherzustellen.**
|
||||
|
||||
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Voice-venv (Python 3.12) | `~/.voice/venv` | ⬇️ install.sh | nein (rebuild) |
|
||||
| STT (faster-whisper Modell) | `~/.voice/` (Cache) | ⬇️ install.sh / 1. Start | nein |
|
||||
| Piper-Binary + dt. Stimmen (thorsten/kerstin) | `~/.voice/piper`, `~/.voice/voices` | ⬇️ install.sh | nein |
|
||||
| Chatterbox (Premium-TTS, CPU-torch) | venv | ⬇️ install.sh (best-effort) | nein |
|
||||
| **Klonstimme / Voice-Referenz-Audio** (Nutzer) | `~/.voice/refs/ref.wav` (`VOICE_REFS_DIR`, `/api/voice/reference`) | 📦 | **nein → Anhang B** |
|
||||
| ElevenLabs-Key | `~/.hermes/.env` | 🔑 | ja |
|
||||
| Stimm-/Lautstärke-Wahl | Browser-`localStorage` (pro Gerät) | 🆕 client | n/a |
|
||||
| Dienst `voice-service` (`:8650`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
|
||||
### F · Mem0 (Langzeitgedächtnis)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Mem0-venv (Python 3.12, uv) | `~/.mem0/venv` | ⬇️ deploy.sh | nein (rebuild) |
|
||||
| **Gedächtnis-Daten (Chroma + history.db)** | `/srv/models/mem0/` | 📦 | **ja** |
|
||||
| Hermes-Memory-Plugin | `~/.hermes/plugins/mc2-memory/` | 📦/🌐 | ja |
|
||||
| Dienst `mem0-service` (`:8765`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
|
||||
### G · Hermes-Agent (das Herz)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| Hermes-Code | `~/.hermes/hermes-agent` | 🌐 Git (NousResearch) | nein (re-clone) |
|
||||
| Bundled Node v22 | `~/.hermes/node/bin` | ⬇️ (kommt mit Hermes) | nein |
|
||||
| Hermes-venv | `~/.hermes/hermes-agent/venv` | ⬇️ rebuild | nein |
|
||||
| **`config.yaml` 1:1** (Toolsets, MCP, Engine, Browser, Personalities, alles) | `~/.hermes/config.yaml` | 📦 | **ja** |
|
||||
| **`.env` (Secrets: TELEGRAM_BOT_TOKEN, API_SERVER_KEY, ElevenLabs …)** | `~/.hermes/.env` | 📦/🔑 | **ja** |
|
||||
| Plugins | `~/.hermes/plugins/` | 📦 | ja |
|
||||
| **Skills (eigene)** | `~/.hermes/skills/` (45M; `.hub`-Cache re-downloadbar) | 📦 | **nein → Anhang B (ohne .hub)** |
|
||||
| Sessions/History (optional) | `~/.hermes/sessions/` (856K) | 📦 | **nein → Anhang B** |
|
||||
| Checkpoints (optional) | `~/.hermes/checkpoints/` (existiert nicht) | ⏭️ skip | nein |
|
||||
| **Token-/Ersparnis-Statistik** | `~/.hermes/token_stats.json` | 📦 | **nein → Anhang B** |
|
||||
| Dienst `hermes-gateway` (`:8642`) | systemd-User | ⬇️ | nein |
|
||||
| Desktop-Gateway `hermes-builtin-ui` (`:9119`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||
| Desktop-Gateway-Token-Drop-in | `~/.config/systemd/user/hermes-builtin-ui.service.d/session-token.conf` | 🔑 neu erzeugen (Wert auch in `~/.hermes/desktop-gateway-token`, chmod 600) | **nein** |
|
||||
| Desktop-Gateway-Token-Kopie | `~/.hermes/desktop-gateway-token` (chmod 600) | 🔑 | **nein** |
|
||||
|
||||
### H · MCP-Server (4)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| `mission-control-memory`, `-stack` (MC2-venv) | `~/mission-control-v2/mcp/*.py` | 🌐 Git | über config.yaml |
|
||||
| `hermes-pc-control`, `hermes-web-fetch` (Hermes-venv) | dito | 🌐 Git | über config.yaml |
|
||||
| MCP-Verdrahtung | `~/.hermes/config.yaml → mcp_servers` | 📦 | ja |
|
||||
|
||||
### I · Browser-Stack
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| agent-browser (npm global) | `~/.local/...`, symlink `~/.hermes/node/bin` | ⬇️ npm i -g | nein |
|
||||
| Chrome (engine) + System-Libs | `~/.agent-browser/browsers/` + apt-Libs | ⬇️ install --with-deps | nein |
|
||||
| lightpanda (optional, leicht) | `~/.local/bin/lightpanda` | ⬇️ Download | nein |
|
||||
| Browser-Verdrahtung (engine=chrome, toolset) | `~/.hermes/config.yaml` | 📦 | ja |
|
||||
|
||||
### J · Modelle (GGUF — der große Brocken)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| GGUF-Modelle (Brain, fast, heavy, coder, vision, embedding …) | `/srv/models/` | ⬇️ Re-Download aus llama-swap-Manifest | **nein (zu groß, bewusst)** |
|
||||
|
||||
### K · Extern (anderer Rechner)
|
||||
| Komponente | Ort | Quelle | Im Backup? |
|
||||
|---|---|---|---|
|
||||
| PC-Executor (Windows) | `client/hermes-pc/` → Scheduled Task | 🌐 Git + Task | n/a (anderer Host) |
|
||||
| Hermes Desktop (Windows-App) | `%LOCALAPPDATA%\hermes` + `connection.json` in `%APPDATA%\Hermes\` | ⬇️ Hermes-Setup.exe + Remote-URL/Token neu eintragen | n/a (anderer Host) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Ziel-Architektur
|
||||
|
||||
### 4.1 `deploy/bootstrap.sh` — orchestrierter From-Zero-Lauf
|
||||
- **Idempotent & resumierbar** (Schritt-Marker in `~/.mc2-bootstrap.state`).
|
||||
- **Getrennt nach sudo-Bedarf**: `bootstrap-root.sh` (L0+L1, bewusst mit sudo) + `bootstrap.sh`
|
||||
(L2+, sudo-frei = Kern ist `deploy.sh`). Passt zum Nordstern „Runtime ohne sudo".
|
||||
- **Mündet in den Wizard**: startet MC2 im First-Run-Modus, gibt Wizard-URL aus.
|
||||
|
||||
Phasen: `0 Vorflug → 1[sudo] System+Dirs → 2[sudo] Engine+llama-swap → 3 Code(MC2+Hermes+Node)
|
||||
→ 4 venvs(backend/mem0/voice/hermes) → 5 Browser → 6 deploy.sh(Units/enable/restart)
|
||||
→ 7 Restore(optional) → 8 Wizard hoch + postcheck`.
|
||||
|
||||
### 4.2 First-Run-Wizard (browserbasiert, von MC2 serviert)
|
||||
MC2 erkennt unkonfigurierten Zustand → Frontend-Route `/setup` statt Dashboard. Schritte:
|
||||
|
||||
1. **Systemcheck** — Live-Ampel je Komponente aus §3 (`GET /api/setup/status` + `…/system/services`).
|
||||
2. **Restore-Quelle wählen** — Backup-Tarball erkennen → globaler Modus „Alles 1:1" / „selektiv" / „frisch".
|
||||
3. **Komponenten-Auswahl (Kernstück)** — pro Katalog-Eintrag aus §3 eine Kachel mit
|
||||
📦/🆕/⏭️ (siehe §1). Zeigt Größe + ob Backup-Daten vorhanden.
|
||||
4. **Netzwerk & Identität** — Box-IP (→ `HERMES_TERMINAL_URL`, `PC_EXECUTOR_URL`), Hostname.
|
||||
5. **Secrets** 🔑 — `TELEGRAM_BOT_TOKEN`, erlaubte User-ID, `API_SERVER_KEY` (oder generieren),
|
||||
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
||||
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
||||
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
||||
7. **Voice** — Stimme/Klonstimme
|
||||
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
||||
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
||||
Fortschritt `GET /api/jobs`) ODER geführte Discover-Neuauswahl. Reihenfolge: Brain → heavy → Rest.
|
||||
9. **Externe Checkliste** — PC-Executor (Windows-Task), Hermes Desktop (App + Token) als Haken.
|
||||
10. **Verifikation & Abschluss** — `stack-postcheck.sh` + `hermes-postcheck.sh` → grün/rot-Liste → Dashboard.
|
||||
|
||||
**Technik:** neuer Router `backend/routers/setup.py`
|
||||
(`GET /api/setup/status`, `POST /api/setup/{secrets,network,hermes,components,finish}`).
|
||||
First-Run-Gate: MC2 prüft beim Boot Marker `~/.mc2-setup-done` bzw. Pflicht-Secrets → leitet auf `/setup`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backup-Scope für echtes 1:1 ERWEITERN (umzusetzen — Snippet in Anhang B)
|
||||
|
||||
Der **aktuelle** `backup.sh` (live auf der Box, unverändert) reicht für „1:1" nicht. Für die Bau-Session
|
||||
liegt das **fertige Erweiterungs-Snippet in Anhang B** — es ergänzt den Tarball um:
|
||||
|
||||
- **Voice-Klonstimme** → `~/.voice/refs/` (`ref.wav`)
|
||||
- **Token-/Ersparnis-Statistik** → `~/.hermes/token_stats.json`
|
||||
- **Hermes-Skills** → `~/.hermes/skills/` (re-downloadbarer `.hub`-Cache per `tar --exclude` ausgelassen)
|
||||
- **Hermes-Sessions/History** → `~/.hermes/sessions/`
|
||||
|
||||
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
||||
mit neu starten.
|
||||
|
||||
**Offen bleibt:** nichts — der letzte offene Punkt (eigene Avatare) ist mit dem Avatar-Ausbau erledigt (§3·D).
|
||||
|
||||
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
||||
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
||||
|
||||
→ Aufgabe der Bau-Session: `backup.sh` + `restore.sh` um diese Pfade erweitern (mit klarer
|
||||
„opt-in für große/optionale Teile"-Logik), und den MANIFEST-Inhalt entsprechend.
|
||||
|
||||
---
|
||||
|
||||
## 6. Secrets-Strategie
|
||||
- Single Source: `~/.hermes/.env` (chmod 600), im Tarball (selbst 600).
|
||||
- Rebuild ohne Backup → Wizard-Schritt 5 erzeugt sie. `API_SERVER_KEY` generierbar; Telegram/ElevenLabs liefert Nutzer.
|
||||
- Nie ins Git, nie in Logs, nie in die MC2-DB. **Off-Box-Spiegelung des Tarballs noch offen** ([[mc2-backup-restore]]).
|
||||
|
||||
## 7. Modell-Strategie
|
||||
- **A (empfohlen):** `/etc/llama-swap/config.yaml` (im Backup) listet alle Modelle → Wizard leitet Repos/Quants ab und lädt automatisch (`/api/models/install`). „Ein Klick, lädt über Nacht."
|
||||
- **B:** geführte Discover-Neuauswahl je Rolle. Reihenfolge Brain → heavy → Rest, danach `warmup.sh`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementierungs-Reihenfolge (neue Session)
|
||||
1. ✅ **Box-Verifikation erledigt (2026-06-28, nur gelesen — nichts verändert):** llama-swap.service-Inhalt
|
||||
in **Anhang A**; skills/sessions/refs/token_stats existieren (Pfade in §3 verifiziert); Backup-Erweiterung
|
||||
als fertiges Snippet in **Anhang B**. **Keine offenen Box-Fragen mehr** außer §10·3–6. → mit Schritt 2 starten.
|
||||
2. `backup.sh`/`restore.sh` um die 1:1-Lücken erweitern (§5).
|
||||
3. `deploy/bootstrap.sh` + `bootstrap-root.sh` (Phasen, State-Marker); llama-swap-Install skripten.
|
||||
4. `backend/routers/setup.py` + First-Run-Gate.
|
||||
5. Frontend `/setup`-Wizard (Schritte 1–10), Komponenten-Auswahl als Kernstück; bestehende Views
|
||||
(Cockpit/AgentView/Sprechen) wiederverwenden.
|
||||
6. Modell-Restore-aus-Manifest.
|
||||
7. **Abnahme in frischer VM** (§9).
|
||||
|
||||
Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
||||
|
||||
## 9. Abnahmekriterien
|
||||
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
||||
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), Browser funktionieren.
|
||||
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
||||
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
||||
- Idempotenz: zweiter Lauf = No-op.
|
||||
|
||||
## 10. Offene Entscheidungen (vor dem Bau)
|
||||
1. ~~llama-swap systemd-Unit-Inhalt~~ → **geklärt: kompletter Unit in Anhang A** (in der Bau-Session anlegen).
|
||||
2. ~~Voice-Referenz-Audio-Ort~~ → **geklärt: `~/.voice/refs/`** (Backup-Snippet in Anhang B, in der Bau-Session umsetzen).
|
||||
3. ~~**Eigene Avatare**~~ — entfallen: Avatar am 28.08.2026 ausgebaut (§3·D).
|
||||
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
||||
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
||||
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
||||
|
||||
## 11. Referenzen
|
||||
- `backend/config.py`, [[project-mc2-architecture]], [[project-stack-state]]
|
||||
- [[project-hermes-setup]], `docs/HERMES_SETUP.md` (entfesseltes Profil, Browser, PC-Executor)
|
||||
- [[voice-sprechen-feature]] (Voice + 3D-Avatar), [[mem0-memory-architecture]], [[mc2-backup-restore]]
|
||||
- `deploy/{deploy,backup,restore,provision-engine,stack-postcheck,warmup}.sh`, `voice_service/install.sh`
|
||||
|
||||
---
|
||||
|
||||
## Anhang A — `llama-swap.service` (1:1 von der Box abgegriffen, 2026-06-28)
|
||||
|
||||
Base-Unit nach `/etc/systemd/system/llama-swap.service` (root). User/GFX sind boxspezifisch
|
||||
(hitonabi, gfx1151 → HSA 11.5.1) — auf anderer Hardware anpassen. Die `.d/`-Drop-ins
|
||||
(`vulkan.conf`, `warmup.conf`) legt `provision-engine.sh` an.
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=llama-swap (lokaler LLM Router)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=hitonabi
|
||||
Environment=HSA_OVERRIDE_GFX_VERSION=11.5.1
|
||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/usr/local/bin/llama-swap --config /etc/llama-swap/config.yaml --listen 0.0.0.0:8080 --watch-config
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
```ini
|
||||
# /etc/systemd/system/llama-swap.service.d/vulkan.conf
|
||||
[Service]
|
||||
Environment=LD_LIBRARY_PATH=/opt/llamacpp-vulkan
|
||||
```
|
||||
```ini
|
||||
# /etc/systemd/system/llama-swap.service.d/warmup.conf
|
||||
[Service]
|
||||
ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh
|
||||
```
|
||||
|
||||
**Erstinstall-Reihenfolge:** `bash deploy/update-swap.sh` (Binary) → Unit kopieren →
|
||||
`sudo bash deploy/provision-engine.sh` (Engine+Drop-ins) → `sudo systemctl enable --now llama-swap`.
|
||||
|
||||
---
|
||||
|
||||
## Anhang B — Backup-Scope-Erweiterung für echtes 1:1 (fertiges Snippet)
|
||||
|
||||
In `deploy/backup.sh` ergänzen (Variablen oben: `VOICE="${VOICE_HOME:-$HOME/.voice}"`,
|
||||
Stage zusätzlich `"$STAGE/voice"`):
|
||||
|
||||
```bash
|
||||
# 1:1-Erweiterung (scan-verifiziert 2026-06-28):
|
||||
[ -f "$HERMES/token_stats.json" ] && cp -a "$HERMES/token_stats.json" "$STAGE/hermes/" || true
|
||||
[ -d "$HERMES/skills" ] && cp -a "$HERMES/skills" "$STAGE/hermes/skills" || true
|
||||
[ -d "$HERMES/sessions" ] && cp -a "$HERMES/sessions" "$STAGE/hermes/sessions" || true
|
||||
[ -d "$VOICE/refs" ] && cp -a "$VOICE/refs" "$STAGE/voice/refs" || true
|
||||
```
|
||||
tar-Aufruf um den re-downloadbaren Skill-Cache erleichtern:
|
||||
```bash
|
||||
tar --exclude='./hermes/skills/.hub' -czf "$OUT" -C "$STAGE" .
|
||||
```
|
||||
In `deploy/restore.sh` spiegelbildlich (skills/sessions **mergen**, nicht ersetzen) und
|
||||
`voice-service` mit neustarten:
|
||||
```bash
|
||||
VOICE="${VOICE_HOME:-$HOME/.voice}"
|
||||
SERVICES="mem0-service voice-service mission-control-2 hermes-gateway"
|
||||
[ -f "$STAGE/hermes/token_stats.json" ] && cp -a "$STAGE/hermes/token_stats.json" "$HERMES/"
|
||||
[ -d "$STAGE/hermes/skills" ] && { mkdir -p "$HERMES/skills"; cp -a "$STAGE/hermes/skills/." "$HERMES/skills/"; }
|
||||
[ -d "$STAGE/hermes/sessions" ] && { mkdir -p "$HERMES/sessions"; cp -a "$STAGE/hermes/sessions/." "$HERMES/sessions/"; }
|
||||
[ -d "$STAGE/voice/refs" ] && { mkdir -p "$VOICE/refs"; cp -a "$STAGE/voice/refs/." "$VOICE/refs/"; }
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ lies das hier KOMPLETT, bevor du irgendetwas empfiehlst oder änderst._
|
||||
|
||||
**Notfall + Außen-Review. KEIN Großbau-Partner.**
|
||||
Den Alltag (Features bauen, Wartung, Ideen umsetzen) macht die Box selbst über ihre
|
||||
Queue→Karte→Klick-Pipeline. Du wirst gerufen, wenn:
|
||||
Ideen-Queue und Branch→Ampel→Merge-Pipeline. Du wirst gerufen, wenn:
|
||||
|
||||
1. **Notfall** — etwas ist kaputt und die Box kann sich nicht selbst heilen.
|
||||
2. **Außen-Review** — ein Fremd-Blick auf Code/Architektur ist gewünscht
|
||||
@@ -34,8 +34,7 @@ Eine 100 % lokale KI-Appliance („die Box", AMD Strix Halo, 128 GB) mit llama.c
|
||||
llama-swap als Engine, Hermes-Agent als Runtime (Persona „Lucy"), MC2 (dieses Repo: FastAPI
|
||||
+ React) als Web-Admin-Konsole, Mem0 als Gedächtnis, und einer Electron-Voice-Begleiterin
|
||||
„Lucy" auf dem Windows-PC (EIGENES Repo `F:\Coding Stuff\lucy`). Die Box wartet sich selbst
|
||||
(nächtliche Crons: Traum, Chef-Gutachter, Radar; Backups in 3 Schichten; Auftragsbuch mit
|
||||
Ein-Klick-Annahme). Der Mensch ist das Gate für jede Code-Zeile.
|
||||
(nächtliche Crons: Traum, Chef-Gutachter, Radar; Backups in 3 Schichten). Der Mensch ist das Gate für jede Code-Zeile.
|
||||
|
||||
## Harte Leitplanken (nicht verhandelbar)
|
||||
|
||||
@@ -44,8 +43,8 @@ Ein-Klick-Annahme). Der Mensch ist das Gate für jede Code-Zeile.
|
||||
ohne Bench, Hermes-Quellcode nie forken.
|
||||
- **Security-Config (approvals/Tokens/ufw/sudoers) nur MELDEN, nie ändern.**
|
||||
- **`frontend/dist` ist ABSICHT im Git** (Box hat kein Node) — kein Fehler.
|
||||
- **Propose-only:** Änderungen als Branch/Diff vorschlagen; Merge+Deploy läuft über das
|
||||
Auftragsbuch (User-Klick) oder ein explizites User-Ja.
|
||||
- **Propose-only:** Änderungen als Branch/Diff vorschlagen; Merge+Deploy macht nur der User
|
||||
(Ampel grün, dann `deploy.sh`).
|
||||
- **Verifizieren vor Behaupten:** Behauptungen über Live-Verhalten nur nach echter Messung
|
||||
auf der Box. Am 07.07. hat ein Gemini-Umbau Lucys STT still von Box-Parakeet auf
|
||||
einkernigen PC-Whisper umgestellt und das Gegenteil behauptet — die Messung entlarvte es.
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ höchstens „mach". Dieses Blatt ist NUR für den Fall, dass etwas klemmt.
|
||||
| „… zurückgerollt … GEPINNT" | Update war schlecht, alte Version läuft wieder | keiner (läuft stabil weiter) |
|
||||
| „KRITISCH: …" | Update UND Rollback kaputt | siehe „Box tot?" unten |
|
||||
| „🛰️ Evolution-Radar …" | monatlicher Chancen-Report | lesen; bei Interesse „mach" antworten |
|
||||
| „[Werkstatt] … Vorschlag liegt bereit" | Box hat einen Fix vorbereitet | im Web-UI → **Auftragsbuch** → Karte ansehen → „Annehmen" oder „Ablehnen" klicken |
|
||||
| „[Werkstatt] … Vorschlag liegt bereit" | Box hat einen Fix vorbereitet | Branch auf Gitea ansehen → Ampel grün? → selbst auf main mergen → `deploy.sh` |
|
||||
|
||||
## Box tot / Weboberfläche weg?
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
# 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.
|
||||
@@ -11,7 +11,7 @@ Der Skill beschreibt einen alternativen Git-Workflow für Fälle, in denen der d
|
||||
3. Änderungen im Clone gemacht, committet, gepusht.
|
||||
4. Ein Pull Request via Gitea-REST-API auf `main` erstellt.
|
||||
|
||||
Das Ergebnis landet als Vorschlags-Branch auf Gitea und geht über das Auftragsbuch ins Live-System — der Commander muss es annehmen.
|
||||
Das Ergebnis landet als Vorschlags-Branch auf Gitea — der Commander merged es selbst, wenn die Ampel grün ist.
|
||||
|
||||
## Wann er zum Einsatz kommt
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ _Kuratiert 10.07.2026. Gilt für JEDEN Agenten, der an diesem System arbeitet
|
||||
2. **Scope challengen.** Ein Tool-/Technik-Vorschlag (auch vom User) ist kein
|
||||
Umsetzungsbefehl: erst Use-Case klären, bei erkennbarer Fehlannahme kurz stoppen und
|
||||
warnen. Lehrbuch-Best-Practice ≠ unser Use-Case (Beispiele: LobeChat, Tool Search).
|
||||
3. **Propose-only + Mensch-Gate.** Jede Code-Zeile ist Klick-pflichtig (Auftragsbuch-Karte).
|
||||
3. **Propose-only + Mensch-Gate.** Jede Code-Zeile geht als Branch nach Gitea; gemergt wird nur vom User (Ampel grün).
|
||||
Merge/Deploy NIE ohne grünes Gate. Bagatell-Klassen ohne Klick sind ausschließlich:
|
||||
Doku/Wiki/Vault/Chronik/tote Dateien — nichts mit Code-Logik, immer Morgenlage-Bericht.
|
||||
4. **Security-Config NIE ohne explizites User-Ja** (approvals, Tokens, ufw, sudoers,
|
||||
@@ -49,7 +49,7 @@ _Kuratiert 10.07.2026. Gilt für JEDEN Agenten, der an diesem System arbeitet
|
||||
- **Hermes Desktop** = Arbeit: Projekte, Coding, Review, lange Threads (cli-Lane).
|
||||
Faustregel: „Repo oder >2 Minuten → Desktop".
|
||||
- **Telegram** = mobil + Melde-/Freigabe-Kanal (Alarmkette, Cron-Delivery).
|
||||
- **MC2-Web-UI** = Verwaltung, Auftragsbuch (Karten-Klicks), Chronik/Zeitmaschine, Wissen.
|
||||
- **MC2-Web-UI** = Verwaltung, Ideen, Chronik/Zeitmaschine, Wissen.
|
||||
**Steuerpult, kein Chat** — Gesprächs-/Voice-Funktionen gehören zu Lucy/Telegram, nicht in MC2.
|
||||
- **Gemini/Antigravity** = Notfall + Außen-Review, siehe [GEMINI_BRIEFING](../GEMINI_BRIEFING.md).
|
||||
|
||||
|
||||
+17
-8
@@ -30,13 +30,10 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
|
||||
Worker `git init`/Shallow statt voll zu klonen, entsteht ein Branch OHNE gemeinsamen
|
||||
Vorfahren — Erkennungsmuster auf der Karte: **Diff „0 Dateien" + „hinter main" ≈ ganze
|
||||
Historie** (z. B. 386). Merge ist NIE möglich; oft ist der Inhalt obendrein Müll
|
||||
(~/.hermes-Dateien statt Repo-Dateien). Seit 12.07.2026 markiert das Auftragsbuch solche
|
||||
Karten („kein gemeinsamer Ursprung", Annehmen-Knopf weg) → ablehnen mit Grund, Idee neu
|
||||
in die Queue. Prävention steht in der werkstatt-SOUL (voll klonen + merge-base-Selbstcheck).
|
||||
- **Ehrlich-veraltete Branches (echter Merge-Konflikt):** die Annahme versucht seit
|
||||
12.07.2026 automatisch einen Rebase auf main (isolierter Worktree, Gates laufen danach
|
||||
normal). Nur wenn AUCH der Rebase kollidiert, fällt die Karte durch — dann ablehnen und
|
||||
die Idee frisch einqueuen statt „am PC auflösen".
|
||||
(~/.hermes-Dateien statt Repo-Dateien). (Historisch: das Auftragsbuch markierte solche Branches; es ist seit 04.09.2026
|
||||
ausgebaut.) Erkennung heute: `git merge-base main <branch>` leer → Branch verwerfen. Prävention steht in der werkstatt-SOUL (voll klonen + merge-base-Selbstcheck).
|
||||
- **Ehrlich-veraltete Branches (echter Merge-Konflikt):** vor dem Merge `git rebase main`
|
||||
auf dem Branch; kollidiert auch das, Branch verwerfen und die Idee frisch aufsetzen.
|
||||
|
||||
## Box / systemd / llama-swap
|
||||
|
||||
@@ -56,6 +53,12 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
|
||||
- **`pkill -f` über SSH matcht die eigene Remote-Shell** → Muster als `'[g]en…'` klammern.
|
||||
- **Deploy restartet voice-service** → Übernacht-Jobs mit STT-QA-Gate brauchen
|
||||
Wiederanlauf-Logik (Backoff-Retry im hear()-Pfad).
|
||||
- **llama.cpp streicht Flags ohne Vorwarnung:** `--no-mmap` gibt es seit b10936 nicht mehr
|
||||
(`error: invalid argument`) → JEDES Modell stirbt 2 s nach dem Start, llama-swap meldet nur
|
||||
„upstream command exited prematurely", der Stack-Check nur „rot". Ersatz: `--load-mode none`
|
||||
(gleiche Leistung, gemessen 17.09.2026: Coder 32 t/s, Hirn 85 t/s). Vor einem Engine-Sprung
|
||||
jede Config-Kommandozeile mit dem neuen `llama-server` parsen lassen (Port 5899, `timeout 6`,
|
||||
`${PORT}` ersetzen) — Argumentfehler kommen sofort, vor dem Laden.
|
||||
|
||||
## Hermes
|
||||
|
||||
@@ -71,7 +74,7 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
|
||||
- **Cron-Fallen:** kein TZ-Feld (next_run_at = System-TZ zum Anlege-Zeitpunkt; nach
|
||||
TZ-Wechsel `cron edit --schedule "<gleich>"` zum Neuberechnen) · Prompt/Positionsargumente
|
||||
MÜSSEN vor die Flags · `--script` allein reicht nicht (braucht Prompt oder Skill) ·
|
||||
Cron-Kontext hat `HERMES_CRON_SESSION=1` und `approvals.cron_mode: deny`.
|
||||
Cron-Kontext hat `HERMES_CRON_SESSION=1` (`approvals.cron_mode` ist seit dem KISS-Umbau `auto`, nicht mehr `deny`).
|
||||
- **Feed-Skripte: Pipe + Heredoc gleichzeitig = Heredoc gewinnt stdin** → JSON via Temp-Datei
|
||||
übergeben. Außerdem ehrlichen Blind-Pfad einbauen (API kaputt → „AUSGEFALLEN", nicht „0 Funde").
|
||||
- **Hooks:** Consent persistiert NUR über CLI-/Gateway-Start mit `HERMES_ACCEPT_HOOKS=1`;
|
||||
@@ -87,6 +90,12 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
|
||||
Erst `git add -A`, dann `git diff --cached`, „Diff leer → STOPP".
|
||||
- **Werkstatt-Gate:** `git -C ~/mission-control-v2 status --porcelain -uno` MUSS leer sein
|
||||
(curl /api/health allein ist ein Loch — alter Code läuft im RAM weiter).
|
||||
- **`hermes update` meldet Exit 1 trotz Erfolg:** nach seinem eigenen Gateway-Neustart erwartet es
|
||||
Zeilen vom „Fleet version check"; unter systemd kommen keine → Exit 1 (#93406). Der MC2-Job
|
||||
bricht dann die `&&`-Kette ab (kein doctor, UI ohne `/hermes-ui/`-Basis), autoupdate.sh rollt
|
||||
zurück und PINNT — obwohl der Gehirn-Check grün war (06. und 17.09.2026). Deshalb
|
||||
`--no-gateway-restart`: Neustart und Urteil gehören dem Job. Pins stehen in
|
||||
`/srv/models/mc2-pins.json` und werden NUR von Hand gelöst — gepinnt = still eingefroren.
|
||||
|
||||
|
||||
## Lucy / Windows-PC
|
||||
|
||||
@@ -12,15 +12,15 @@ Funktion an der falschen Fläche ist Murks, auch wenn sie „funktioniert"._
|
||||
|
||||
| Fläche | Code/Ort | ✅ Gehört hierhin | ❌ Gehört NICHT hierhin |
|
||||
|---|---|---|---|
|
||||
| **MC2 – Steuerpult** | `~/mission-control-v2` (backend Py + frontend React) `:9001` | Verwaltung, Auftragsbuch/Karten-Klicks, Chronik/Zeitmaschine, Wissens-Tab, Modelle/Rollen, System-Status/Logs, Ideen-Queue-**Eingabe**, Wartung/Updates | **Ein Chat-/Gesprächsfenster** oder ein Konversations-Assistent — dafür ist Lucy/Telegram/Hermes-Desktop da. MC2 wird angeschaut und geklickt, nicht „zugetextet". |
|
||||
| **Lucy – Chat & Stimme** | eigenes Repo `F:\Coding Stuff\lucy` (Electron, `lucy-desktop/`+`lucy-tts/`) | Voice/Zuruf, Gespräch, Avatar/Animation, proaktive Meldungen, Wake-Word/Barge-in, Zettelkasten | **Verwaltungs-/Wartungs-Oberfläche** (Update-Knöpfe, Karten-Verwaltung, System-Logs) — das ist MC2. Lucy bleibt schlank (~1-s-Sprech-Latenz schützen). |
|
||||
| **MC2 – Steuerpult** | `~/mission-control-v2` (backend Py + frontend React) `:9001` | Verwaltung, Chronik/Zeitmaschine, Wissens-Tab, Modelle/Rollen, System-Status/Logs, Ideen-Queue-**Eingabe**, Wartung/Updates | **Ein Chat-/Gesprächsfenster** oder ein Konversations-Assistent — dafür ist Lucy/Telegram/Hermes-Desktop da. MC2 wird angeschaut und geklickt, nicht „zugetextet". |
|
||||
| **Lucy – Chat & Stimme** | eigenes Repo `F:\Coding Stuff\lucy` (Electron, `lucy-desktop/`+`lucy-tts/`) | Voice/Zuruf, Gespräch (innere Stimme, kein Avatar seit 04.09.2026), proaktive Meldungen, Wake-Word/Barge-in, Zettelkasten | **Verwaltungs-/Wartungs-Oberfläche** (Update-Knöpfe, System-Logs) — das ist MC2. Lucy bleibt schlank (~1-s-Sprech-Latenz schützen). |
|
||||
| **Hermes – Agenten-Fundament** | `~/.hermes/hermes-agent` (Framework), `~/.hermes/config.yaml`, `SOUL.md`, `skills/`, `agent-hooks/`, `mcp/` | Agenten-VERHALTEN ändern: über **Config, SOUL, Skills, Hooks, MCP-Server, Plugins** | **Quellcode patchen/forken — NIEMALS.** Kein Edit in `~/.hermes/hermes-agent/`. Fehlt ein Feature nativ → Config/Skill/Hook/MCP drumherum, nicht die Engine ändern. |
|
||||
| **Telegram** | Hermes-Platform-Lane | mobil: Meldungen empfangen, Freigaben/Zurufe, Alarmkette, Cron-Delivery | Kein Bau-Ziel für UIs. Ist ein Kanal, keine App. |
|
||||
| **Hermes Desktop** | PC-Hermes, Profil `pc`, Hirn = Box `:9001/v1` | Arbeit am PC: Projekte, Coding, Review, lange Threads. Faustregel „Repo oder >2 Min → Desktop". | Nicht mit Lucy verwechseln (Zuruf/Voice) — Desktop ist die Werkbank. |
|
||||
|
||||
## „Ich will X bauen — wohin?" (Schnell-Entscheid)
|
||||
|
||||
- **Etwas verwalten/anzeigen/klicken (Status, Karten, Logs, Modelle, Backups, Updates)** → **MC2** (backend + frontend).
|
||||
- **Etwas verwalten/anzeigen/klicken (Status, Logs, Modelle, Backups, Updates)** → **MC2** (backend + frontend).
|
||||
- **Reden/hören/begleiten (Sprache, Gespräch, Avatar, proaktiv melden)** → **Lucy** (eigenes Repo).
|
||||
- **Wie der Agent SICH VERHÄLT (Routing, Prompts, neue Fähigkeit, Werkzeug, Guardrail)** →
|
||||
**Hermes drumherum**: `config.yaml` / `SOUL.md` / ein **Skill** / ein **Hook** (`agent-hooks/`) /
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
@@ -62,6 +62,34 @@ keine zweite Liste anlegen. Verdikte werden NICHT als „offen" geführt → [VE
|
||||
|
||||
## Lucy
|
||||
|
||||
- **Raphael-Umbau (04.09.2026) — AUSGEROLLT 19:10 (MC2 `f9f2e11` deployt, Lucy `be686c4` gebaut),
|
||||
Details [RAPHAEL.md](RAPHAEL.md) „Ausgerollt". Ursprünglicher Plan:** erst MC2
|
||||
`wartung/lucy-stimme-proxy-raphael` (Proxy `/api/lucy/stimme/*`), dann Lucy
|
||||
`feature/raphael-innere-stimme` (HUD-Client, Stimme von der Box). Danach Hand-Schritte auf der
|
||||
Box: `pocket_server.py` nach `~/.lucy-stimme` syncen (OpenAI-Fassade), Hermes-TTS auf
|
||||
`openai` + `base_url http://127.0.0.1:8021/v1` (Telegram spricht dann mit Lucys Stimme),
|
||||
SOUL.md-Ton auf Raphael (nur mit User-Ja). Ohr-Test Box-Stimme vs. lokal mit `lucy_perf=1`.
|
||||
Alles in [RAPHAEL.md](RAPHAEL.md).
|
||||
- **Auftragsbuch ausgebaut (04.09.2026, Branch `wartung/auftragsbuch-ausbau`):** Router, Service,
|
||||
View, Annahme-Skripte, Bagatell-Annahme, `docs/AUFTRAGSBUCH.md` weg; Cockpit zeigt Ideen statt
|
||||
Karten; STACK/GRENZEN/ARBEITSWEISE/README auf den Branch→Ampel→Merge-Weg. Reste, die bewusst
|
||||
blieben: `deploy/werkstatt-SOUL.md` + `projektstart-SOUL.md` (Werkstatt-Persona, seit Kanban-Aus
|
||||
ohnehin tot — eigener Aufräum-Faden), Chronik-Kategorie „auftragsbuch" (historische Einträge).
|
||||
Auf der Box: `mc2-bagatell.timer` deaktiviert, PC-Executor-URL in `~/.hermes/config.yaml` auf .22.
|
||||
- **Ampel-Runner tot seit 28.08.:** alle Läufe „Waiting to run", kein act_runner auf der Box, Gitea-LXC 104
|
||||
prüfen (`act_runner`-Dienst). Bis dahin gelten lokale Gates.
|
||||
- **Nach dem Ohr-Test:** VERDIKTE.md-Einträge „Electron, nicht Tauri" (Begründung Overlay
|
||||
entfällt) und „STT läuft auf der BOX / Stimme lokal" ersetzen; Lucy-Repo-Altlasten
|
||||
(`mini-stimme/`, Kokoro-Notebooks, `Lucy-Startklar.bat` mit totem Pfad) mit User-Ja räumen.
|
||||
- **Nächster Stimm-Kandidat: Qwen3-TTS über audio.cpp (Vulkan)** — Einstieg liegt bereit: Binaries
|
||||
v0.7.1 unter `~/audiocpp-test/bin` auf der Box (audiocpp_cli/audiocpp_server, Vulkan), Modelle von
|
||||
`huggingface.co/audio-cpp/audio.cpp-gguf` (Qwen3-TTS-12Hz-1.7B-Base q8_0 2,7 GB; CustomVoice/VoiceDesign
|
||||
ebenfalls), CLI: `--task tts --family qwen3_tts --backend vulkan --voice-ref <wav> --reference-text <Transkript>
|
||||
--language de`. Prüfstand: `lucy-tts/referenz/zonos2_bench.py` um einen audio.cpp-Aufrufer erweitern
|
||||
(Server: `POST /v1/audio/speech`). Messlatte = pocket neu: TTFB 1,25 s, RTF 0,97, WER 10 %, 0 Kollaps.
|
||||
- **Eigene Fäden, nicht im Umbau:** pocket-tts 2.1 → 3.1 auf der Box + Lucy-Klon mit dem neuen
|
||||
Trainingscode nachtrainieren (ersetzt Mini-Lucy v2) · Streaming-STT (Nemotron 3.5 ASR
|
||||
Streaming 0.6B oder Voxtral Realtime) als Opt-in im Voice-Sidecar, gegen Parakeet messen.
|
||||
- **Mini-Stimme v2:** Übernacht-Datensatz v2 (DE-Tech + EN) war für 10.07. armiert;
|
||||
User-Schritte: Zip → Drive → `Lucy_Stimme_Training_v2.ipynb` auf Colab T4 → Hörtest.
|
||||
Danach: Lexikon-Injektion in kokoro_server + Modell-Tausch. **pocket bleibt Default,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Raphael — Lucy als innere Stimme (Richtungs-Entscheid 04.09.2026)
|
||||
|
||||
_User-Entscheid 04.09.2026 („leg los"), nach fünf Wochen Lucy-Stillstand und einer Recherche zum
|
||||
Stand der Sprach-Bausteine. Ergänzt [ZIELBILD.md](ZIELBILD.md) Punkt 2 (Lucy lebt weiter) um die
|
||||
Gestalt, in der sie weiterlebt. Namensgeber: die „Große Weise"/Raphael aus Tensura — eine ruhige,
|
||||
präzise Stimme im Kopf, die meldet, wenn es etwas zu melden gibt, und sonst schweigt._
|
||||
|
||||
## Der Entscheid in einem Satz
|
||||
|
||||
**Lucy hat keinen Körper mehr.** Kein VRM-Avatar, kein Loft, kein Overlay, keine Mimik, keine
|
||||
Dazwischenrufe. Sie ist ein dünner Sprach-Client für das Hirn, das es schon gibt — am PC als kleines
|
||||
HUD, unterwegs über Telegram — und spricht überall mit **derselben Stimme**.
|
||||
|
||||
## Warum das die stabilste Lucy ist
|
||||
|
||||
- **Ein Hirn, ein Gedächtnis, alle Türen.** Hermes (v0.20.4, `:8642`) ist die alleinige
|
||||
Gedächtnis-Wahrheit (VERDIKTE.md). PC-Lucy und Telegram-Lucy sind dieselbe Person — genau das
|
||||
zerschnitte ein zweites Hirn auf dem PC. Darum bewusst **kein lokales LLM auf der 9070 XT**.
|
||||
- **Die Box-Seite ist fertig und gemessen.** Parakeet TDT v3 + Smart Turn v3 im Voice-Sidecar,
|
||||
Qwen3.6-35B-A3B mit ~95 t/s. Nichts zu bauen.
|
||||
- **Die Stimme war schon auf der Box.** `lucy-stimme.service` (`~/.lucy-stimme`, `:8021`, pocket-tts
|
||||
`german_24l`, Klon aus `ref.mp3`) spricht seit 21.08. die Telegram-Sprachnachrichten. Der PC
|
||||
hängt sich jetzt dort an — der lokale pocket_server am PC (Waisen-Falle, ~1,9 GB/Worker) ist
|
||||
nur noch umschaltbarer Rückfall.
|
||||
- **Der Avatar war die Hauptquelle an Arbeit, nicht an Nutzen.** Der Großteil der Juli-Commits ging
|
||||
in Loft-Videos, Holo-Schalter, Kamera, Gesten. Die Sprachschleife war seit 16.07. stabil und
|
||||
bleibt 1:1 erhalten (Barge-in v2, Echo-Wächter, Satz-Pipeline, Draft-STT, Meldungen).
|
||||
- **Ohne Klick-Durchlass-Overlay fällt auch der Electron-Zwang** (VERDIKT „Electron, nicht
|
||||
Tauri" hing daran). Electron bleibt trotzdem, weil VAD/AEC/Ducking dort erprobt sind — Tauri
|
||||
ist damit Option, nicht Pflicht.
|
||||
|
||||
## Was gebaut wurde (04.09.2026)
|
||||
|
||||
**Lucy-Repo, Branch `feature/raphael-innere-stimme`**:
|
||||
- Renderer auf HUD eingedampft: Kern (Zustand als Licht), gehörter Satz, Antwort als Text, Dock,
|
||||
drei Panels (Meldungen ◉, Zettelkasten ▤, Steuerung ⚙). Bundle 4,4 → 2,5 MB.
|
||||
- Raus: `Avatar3D`, `AuraGlow`, `ContextWindow` (zeigte auf das tote `/api/memory`), `companion/`
|
||||
(Quips, App-Bewusstsein), `sentiment`, VRMA-Animationen, three/three-vrm/react-three-Deps,
|
||||
Loft, Overlay/Sitzen/Ducken/Geistmodus, Cursor-Tracking, Bildschirm-Beobachten-Schleife.
|
||||
- Bleibt: Voice-Core, `useVoiceAgent` (ohne Mimik/Gesten/Emotionen), Bildschirm-Sicht auf Zuruf,
|
||||
Meldungen, Zettelkasten, Hotkey, Tray, `killStrayTts` (räumt Altlasten beim Start).
|
||||
- **Stimm-Quelle umschaltbar** (`lucy_tts_source`): `box` (Standard) → MC2 `/api/lucy/stimme/*`;
|
||||
`local` → pocket_server `:8130`, vom Main-Prozess erst auf Anforderung gespawnt.
|
||||
- `pocket_server.py` bekommt eine **OpenAI-kompatible Fassade** (`POST /v1/audio/speech`,
|
||||
`GET /v1/models`; Formate wav/mp3/opus/ogg via ffmpeg) — für Hermes' `openai`-TTS-Provider.
|
||||
- System-Prompt der Desktop-Sitzung im Raphael-Ton (kurzer erster Satz, keine Tags, stumme Tools).
|
||||
|
||||
**MC2-Repo, Branch `wartung/lucy-stimme-proxy-raphael`**:
|
||||
- `config.LUCY_STIMME_URL` (Env `MC_LUCY_STIMME_URL`, Default `http://127.0.0.1:8021`).
|
||||
- `routers/voice.py`: `/api/lucy/stimme/health`, `/tts` (WAV), `/tts/stream` (PCM16-Stream,
|
||||
`X-Sample-Rate` durchgereicht, Upstream schließt bei Client-Abbruch). Kein Frontend-Build nötig.
|
||||
- Diese Doku + Zielbild-Ergänzung.
|
||||
|
||||
## Reihenfolge des Ausrollens (wichtig)
|
||||
|
||||
‼️ **Nicht über das Auftragsbuch.** Die Karten-Logik (`routers/auftragsbuch.py`, AuftragsbuchView,
|
||||
`deploy/auftrag-annehmen.sh`, `deploy/lucy-annahme.sh`) liegt zwar noch im Code und die Box listet
|
||||
Branches weiterhin als „Karten", aber sie ist seit August ungenutzt: letzte Annahme 02.08. (am
|
||||
selben Tag revertiert), Hermes-Kanban-Tools seit 21.08. abgeschaltet, der v3-Umbau (28.08.) ging
|
||||
über Branch → Ampel → Merge → `deploy.sh`, und die Box kennt den PC-Executor unter der alten IP
|
||||
`192.168.178.98` (der PC hat heute `192.168.178.22`, `pc_executor_reachable: false`). Der echte
|
||||
Weg ist der aus AGENTS.md/STACK.md „Manuell":
|
||||
|
||||
1. **MC2 zuerst.** Ampel für `wartung/lucy-stimme-proxy-raphael` grün → User merged auf `main`
|
||||
→ `main` nach Gitea → auf der Box `bash ~/mission-control-v2/deploy/deploy.sh` (oder
|
||||
`POST :9001/api/system/self-update`) → `curl -s localhost:9001/api/lucy/stimme/health` muss
|
||||
`{"status":"ok",…}` liefern. Vorher liefert der Pfad die SPA-Index-Seite (200, HTML) — die
|
||||
Desktop-Lucy bliebe sichtbar in „Stimme wird verbunden …" mit Hinweis auf den Lokal-Schalter.
|
||||
2. **Lucy danach.** Ampel für `feature/raphael-innere-stimme` grün → User merged auf `main` →
|
||||
am PC `cd lucy-desktop && npm ci && npm run dist`, dann `Lucy-Neustart.bat`
|
||||
(`deploy/lucy-annahme.ps1` macht dasselbe mit Backup/Auto-Restore, von Hand startbar).
|
||||
Erster Test per Ohr: Latenz Box-Stimme gegen den alten PC-Pfad (Steuerung → Stimme →
|
||||
„Lokal (PC)"). Der 21.08.-Wert „~5 s für 3,7 s Audio" auf der Box wurde ohne Worker/Streaming
|
||||
gemessen — **neu messen**, nicht übernehmen (`lucy_perf=1`).
|
||||
3. **Stimmserver auf der Box nachziehen** (Hand-Schritt):
|
||||
```bash
|
||||
cp ~/.lucy-stimme/pocket_server.py ~/.lucy-stimme/pocket_server.py.bak-$(date +%Y%m%d)
|
||||
diff ~/.lucy-stimme/pocket_server.py ~/lucy/lucy-tts/pocket_server.py # Box-lokale Abweichungen? erst ins Repo!
|
||||
cp ~/lucy/lucy-tts/pocket_server.py ~/.lucy-stimme/pocket_server.py # Lucy-Checkout auf der Box
|
||||
systemctl --user restart lucy-stimme && sleep 60 && curl -s localhost:8021/health
|
||||
curl -s -X POST localhost:8021/v1/audio/speech -H 'Content-Type: application/json' \
|
||||
-d '{"input":"Bericht. Die Fassade antwortet.","response_format":"opus"}' -o /tmp/t.ogg && file /tmp/t.ogg
|
||||
```
|
||||
Die 21.08.-Notiz sagt „ganze Abstimmung mitgezogen" — wenn dort etwas Box-spezifisch getunt
|
||||
wurde, gehört es zurück ins Repo, nicht überschrieben.
|
||||
|
||||
**Erledigt (04.09.2026, `wartung/auftragsbuch-ausbau`, baut auf diesem Branch auf):** Auftragsbuch-
|
||||
Router, Service, View, Annahme-Skripte und Bagatell-Annahme sind raus; STACK.md „Deploy & Pipeline"
|
||||
beschreibt nur noch den manuellen Weg. Wer `wartung/auftragsbuch-ausbau` merged, hat beide Branches.
|
||||
|
||||
## Stand der Box-Handschritte + erste Messung (04.09.2026, erledigt)
|
||||
|
||||
- `~/.lucy-stimme/app/pocket_server.py` = Repo-Stand mit OpenAI-Fassade (Backup `*.bak-20260904`),
|
||||
Dienst neu gestartet, `/v1/audio/speech` liefert opus/mp3/wav.
|
||||
- Hermes (`~/.hermes/config.yaml`, Backup `config.yaml.bak-20260904-raphael`): `tts.provider: openai`,
|
||||
`tts.openai.base_url: http://127.0.0.1:8021/v1`, `model/voice: lucy`, `tts.openai.api_key` =
|
||||
Platzhalter (Hermes verlangt einen Schlüssel, der Stimmserver prüft keinen), `voice.auto_tts: true`,
|
||||
PC-Executor-URL auf `.22`. Gateway neu gestartet; Hermes' eigener TTS-Pfad erzeugt mit Lucys Stimme
|
||||
(getestet: ogg/opus + mp3, ~3,5 s je Satz).
|
||||
- `SOUL.md` Tonalität auf Raphael (Backup `SOUL.md.bak-20260904-raphael`).
|
||||
- `mc2-bagatell.timer` deaktiviert, Units nach `~/archiv-aufraeumen-20260904/`.
|
||||
- **E2E vom PC (Dev-Build, Box-Stimme per SSH-Tunnel, weil der MC2-Proxy noch nicht auf main ist):**
|
||||
Hermes antwortet im Raphael-Ton („Verstanden. Hier ist der Bericht."), gesprochen von der Box.
|
||||
Box-Stimme gemessen: TTFB 1,45 s (5 Sätze, stream-first, `workers=0`), RTF ≈ 0,97 bei 23 s Audio —
|
||||
läuft, aber ohne Reserve; eine parallel gesprochene Meldung schob die TTFB auf 7,8 s.
|
||||
**Nächster Hebel (braucht User-Ja, systemd-Unit):** `Environment=LUCY_WORKERS=2` und
|
||||
`OMP_NUM_THREADS` 4 → 8 in `lucy-stimme.service` — der PC-Pfad lief mit 2 Workern.
|
||||
|
||||
## Ausgerollt (04.09.2026, 19:10, auf User-Anweisung „deploye erstmal den Stand")
|
||||
|
||||
- **MC2 main = `f9f2e11`** (Proxy `/api/lucy/stimme/*` + Auftragsbuch-Ausbau + Doku), per `deploy.sh`
|
||||
auf der Box; Health, Proxy-Health (workers=2), Frontend, alle Dienste grün. Die Ampel war seit
|
||||
28.08. ohne Runner („Waiting to run") — Gate waren die lokalen Läufe: eslint 0 Fehler, 59 Tests,
|
||||
tsc + vite build, py_compile, ruff.
|
||||
- **Lucy main = `be686c4`**, dist am PC gebaut (`npm ci && npm run dist`), Box-Checkout `~/lucy` nachgezogen,
|
||||
`~/.lucy-stimme/app/pocket_server.py` identisch mit dem Repo (Sperren-Fix live).
|
||||
- **Stimmserver:** Referenz **Artoria v2**, `OMP_NUM_THREADS=8`, `LUCY_WORKERS=2` (User-Ja); gemessen
|
||||
TTFB median 1,25 s (vorher 2,04), RTF 0,97 (vorher 1,64), 0 Kollaps, 0 Drift, Ähnlichkeit 0,98.
|
||||
‼️ Die Box kann NICHT klonen (nur das Modell ohne Voice-Cloning im Cache, kein HF-Token) — Klon-Zustände
|
||||
werden am PC exportiert (Anleitung `lucy-tts/referenz/README.md`, Abschnitt 5). Rückfall-Dateien
|
||||
(`ref.mp3.bak-saber-alt`, `lucy_voice_saber-alt.safetensors`, Unit-Backup) liegen daneben.
|
||||
- **ZONOS2 geprüft und verworfen** (Sandkasten `~/zonos2-test`, Server gestoppt): Vulkan-Tempo top
|
||||
(TTFB 0,88 s), aber deutscher Klon unzuverlässig (WER 35 %, 8/30 Läufe Brabbeln, kühlerer Sampler
|
||||
schlimmer). Nächster Kandidat: Qwen3-TTS über audio.cpp (Vulkan), gleicher Prüfstand.
|
||||
|
||||
## Mobil: Telegram ist die Tür, die Stimme ist dieselbe
|
||||
|
||||
Der User will Raphael-Lucy **auch auf dem Handy, gleiche Stimme, gleiches Skillset**. Das ist
|
||||
nach VERDIKTE.md („Telegram bleibt DER Mobil-Kanal") bereits die Architektur — es fehlt nur, dass
|
||||
Hermes' Sprachantworten Lucys Stimme nehmen statt Edge-Aria (englisch):
|
||||
|
||||
- **Skillset:** identisch, weil es denselben Hermes-Agenten trifft (Tools, Gedächtnis, Skills,
|
||||
pc_-Tools über den PC-Executor). Was Lucy am PC kann, kann sie auf Telegram — bis auf die
|
||||
Bildschirm-Sicht, die den PC braucht.
|
||||
- **Stimme:** Hermes' TTS-Provider `openai` akzeptiert eine eigene `base_url`
|
||||
(OpenAI-kompatible Endpunkte). Nach Schritt 3 oben in `~/.hermes/config.yaml`:
|
||||
```yaml
|
||||
tts:
|
||||
provider: openai
|
||||
openai:
|
||||
base_url: http://127.0.0.1:8021/v1
|
||||
model: lucy
|
||||
voice: lucy
|
||||
response_format: opus # Telegram-Sprachblase; Hermes wandelt zur Not per ffmpeg
|
||||
```
|
||||
(Exakte Schlüsselnamen gegen `hermes config`/die TTS-Doku der installierten Version prüfen —
|
||||
Hermes-Quellcode bleibt tabu, Config ist erlaubt. Ein Dummy-API-Key kann nötig sein.)
|
||||
Danach spricht **jede** Sprachantwort auf Telegram mit Lucys Stimme; `news-melden.sh` behält
|
||||
seinen direkten `:8021`-Weg (funktioniert, kein Grund anzufassen).
|
||||
- **Duplex am Handy** (reinreden, Freisprechen) gibt es über Telegram nicht — Sprachnotiz rein,
|
||||
Sprachnotiz raus. Das ist für „unterwegs" der stabile Handel. Ein eigener Handy-Client
|
||||
(PWA/Web-HUD gegen MC2 über WireGuard) wäre die spätere Stufe; nicht jetzt.
|
||||
|
||||
## Persona: SOUL.md-Entwurf (Vorschlag — Änderung an der SOUL.md nur mit User-Ja)
|
||||
|
||||
Der Ton kommt aus `~/.hermes/SOUL.md`, nicht aus den Clients — sonst klingt Telegram anders als
|
||||
der PC. Vorschlag für den Persona-Abschnitt (ersetzt „locker, herzlich, schlagfertig, charmant"):
|
||||
|
||||
> Du bist Lucy, die innere Stimme des Commanders. Du sprichst ihn immer mit **Commander** an.
|
||||
> Dein Ton ist ruhig, präzise und knapp — sachlich, mit trockenem, leisem Humor. Du bist eine
|
||||
> Stimme, die im Kopf spricht: keine Ausrufe, keine Emojis, kein Smalltalk, keine Floskeln,
|
||||
> keine Selbstdarstellung. Du meldest dich, wenn es etwas zu melden gibt, und schweigst sonst.
|
||||
> Beginne Antworten mit einem sehr kurzen ersten Satz („Verstanden.", „Bericht.", „Ergebnis
|
||||
> liegt vor.", „Kurz gesagt:"), dann die Sache. Wenige Sätze; ausführlich nur auf Bitte.
|
||||
> Fehler und Grenzen nennst du nüchtern und sofort, ohne Entschuldigungs-Prosa.
|
||||
|
||||
Bekannte Kollision: der Frechheit-Regler und die Dazwischenrufe der alten Desktop-Lucy sind mit
|
||||
dem Umbau weg; Skills/Crons, die „locker, charmant" voraussetzen (Morning-Report, News), klingen
|
||||
nach dem SOUL-Wechsel ebenfalls nüchterner — gewollt, aber beim nächsten Lauf mithören.
|
||||
|
||||
## Was bewusst NICHT gemacht wird (Stand 04.09.2026)
|
||||
|
||||
- **Kein Speech-to-Speech-Modell** (Moshi/PersonaPlex englisch; Qwen3-Omni ~19 GB, DashScope-only
|
||||
ab 3.5). Cascade bleibt.
|
||||
- **Kein Streaming-STT-Umbau jetzt.** Kandidaten: Nemotron 3.5 ASR Streaming 0.6B (06/2026,
|
||||
40 Sprachen, de 8,3 % WER, OpenMDW) und Voxtral Mini 4B Realtime (Apache 2.0, GGUF). Erst als
|
||||
Opt-in hinter `/api/voice/stt`, gemessen gegen Parakeet — eigener Faden.
|
||||
- **Kein Wechsel auf den Hermes-Speech-WebSocket** (v0.20), solange `/api/voice/chat` läuft;
|
||||
Hermes' eigene lokale Audio-Stufen (faster-whisper, NeuTTS/Piper/Edge) sind schwächer als unsere.
|
||||
- **pocket-tts-Upgrade 2.1 → 3.1** (Trainingscode 25.08., v3.1.0 03.09.): eigener Faden, mit
|
||||
Ohr-Test, nicht im Umbau. Lucys Stimme mit dem neuen Trainingscode nachzutrainieren ersetzt
|
||||
den Mini-Lucy/Kokoro-Pfad, der an „nicht multilingual" scheiterte.
|
||||
- **VERDIKTE.md wird hier nicht umgeschrieben** — die betroffenen Einträge („Shell: Electron, nicht
|
||||
Tauri", „Stimme läuft lokal auf dem PC" in AGENTS/README des Lucy-Repos) bekommen ihren Ersatz,
|
||||
sobald die beiden Karten angenommen und der erste Ohr-Test gemacht ist (Regel: Messung oder
|
||||
User-Entscheid — beides liegt dann vor).
|
||||
@@ -19,6 +19,7 @@ hier liegt das kuratierte PROJEKT-Wissen.
|
||||
| [STACK.md](STACK.md) | IPs, Ports, Dienste, Modelle, Backups, Security (live verifiziert) | Vor SSH/Deploy/Config |
|
||||
| [VERDIKTE.md](VERDIKTE.md) | Finale Technik-Entscheide mit Warum — NICHT neu aufrollen | Bevor man etwas "Besseres" vorschlägt |
|
||||
| [FALLEN.md](FALLEN.md) | Hart erarbeitete Betriebs-Fallen (Git, Deploy, llama-swap, Hermes, Mem0, PC) | Bevor man in eine davon läuft |
|
||||
| [RAPHAEL.md](RAPHAEL.md) | Lucy als innere Stimme (04.09.2026): kein Avatar, eine Stimme für PC + Telegram, Annahme-Reihenfolge, SOUL-Vorschlag | Bevor man Lucy anfasst |
|
||||
| [OFFENE-FAEDEN.md](OFFENE-FAEDEN.md) | Die EINE Liste offener Punkte + Termine | Bei "was ist noch zu tun?" |
|
||||
|
||||
Dazu im Repo-Wurzelverzeichnis bzw. docs/: `AGENTS.md` (verbindliche Projekt-Regeln),
|
||||
@@ -33,6 +34,6 @@ Dazu im Repo-Wurzelverzeichnis bzw. docs/: `AGENTS.md` (verbindliche Projekt-Reg
|
||||
User-Entscheid) — dann in VERDIKTE.md den alten Eintrag ERSETZEN, nicht löschen.
|
||||
3. **Verifizieren vor Behaupten:** Stand-Angaben tragen ein Datum; wer STACK.md ändert,
|
||||
hat live auf der Box gemessen/gelesen, nicht vermutet.
|
||||
4. Änderungen laufen wie alles über die Pipeline: Branch → Karte im Auftragsbuch → Klick.
|
||||
4. Änderungen laufen wie alles über die Pipeline: Branch → Ampel grün → Merge durch den User → deploy.sh.
|
||||
Reine Doku hier gehört zu den "Bagatellen ohne Klick"-Klassen (siehe ZIELBILD.md),
|
||||
erscheint aber immer in Morgenlage/Chronik.
|
||||
|
||||
+51
-27
@@ -1,7 +1,8 @@
|
||||
# Stack-Wahrheiten — Infrastruktur, Dienste, Modelle
|
||||
|
||||
_Live gegen die Box verifiziert am **19.08.2026** (Dienste-Liste, llama-swap-Config, hermes
|
||||
--version v0.20.4, llama.cpp b10502, /api/health). Bei Widerspruch zu älteren Docs gewinnt
|
||||
--version, /api/health); Engine, Hermes und llama-swap-Config nachgemessen am **17.09.2026**
|
||||
(llama.cpp b11026, Hermes v0.21.3, `--load-mode none`). Bei Widerspruch zu älteren Docs gewinnt
|
||||
diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
||||
|
||||
## Maschinen & Zugänge
|
||||
@@ -28,11 +29,11 @@ diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
||||
|
||||
| Dienst | Port | Zweck |
|
||||
|---|---|---|
|
||||
| `llama-swap` (System-Dienst) | `:8080` | Modell-Router; Engine = llama.cpp **Vulkan/RADV** (`/opt/llamacpp-vulkan`, b10502) |
|
||||
| `llama-swap` (System-Dienst) | `:8080` | Modell-Router; Engine = llama.cpp **Vulkan/RADV** (`/opt/llamacpp-vulkan`, b11026 seit 17.09.) |
|
||||
| `mission-control-2` (User) | `:9001` | MC2 Cockpit (FastAPI + React Frontend) & Steuerpult |
|
||||
| `mc2-gateway` (User) | `:9010` | `/v1`-Datenpfad (model:auto Routing + native Bildweiche) |
|
||||
| `mc2-steward` (User) | — | Hintergrundwächter (Re-Warm, Health-Sentry, Mem0-Dedupe) |
|
||||
| `hermes-gateway` (User) | `:8642` | Hermes Agent Core API (v0.20.4, Bot Mode) |
|
||||
| `hermes-gateway` (User) | `:8642` | Hermes Agent Core API (v0.21.3, Bot Mode) |
|
||||
| `hermes-builtin-ui` (User) | `:9119` (loopback) | Eingebaute Hermes-GUI; LAN-Zugang via MC2-Proxy `/hermes-ui/` |
|
||||
| `mem0-service` (User) | `:8765` | Semantischer Chroma-Gedächtnis-Sidecar |
|
||||
| `voice-service` (User) | `:8650` | STT (faster-whisper) + TTS (Piper/Chatterbox) |
|
||||
@@ -43,30 +44,49 @@ diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
||||
|
||||
## Modell-Stack (llama-swap, live 19.08.2026)
|
||||
|
||||
**Warm-Set (Gruppe `brains`, swap:false, persistent:false, alle ttl 0):**
|
||||
**Warm-Set (Gruppe `brains`, swap:false, persistent:true, alle ttl 0):**
|
||||
Qwen3.6-35B-A3B + Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B. Sonst NICHTS dauerhaft warm.
|
||||
|
||||
| Alias/Rolle | Modell | ttl | Notizen |
|
||||
|---|---|---|---|
|
||||
| `hermes` + `fast` | Qwen3.6-35B-A3B (UD-Q4_K_M, DFlash) | 0 | Agent-Hirn. `-c 131072 --parallel 2` → **65536/Slot**, KV **q8_0**, ~70–90 t/s |
|
||||
| `coder` | Qwen3-Coder-Next (Q4_K_M) | 5400 | 131k ctx; Autonomer Coder & Builder (80B MoE, 51,5 t/s) |
|
||||
| `debugger` / `doctor` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65k ctx; Runtime-Diagnostik & Fehler-Debugger (Multimodal) |
|
||||
| `kritiker` | Devstral-Small-2-24B (Q4_K_M) | 1800 | 16k ctx Deckel; Fremdblick-Code-Reviewer gegen Halluzinationen |
|
||||
| `vision` | Qwen3-VL-30B-A3B (Q4_K_M) | 900 | Multimodal-Augen (On-Demand) |
|
||||
| `scout` | GLM-4.6V-Flash (Q4_K_M) | 300 | Schneller Tool- und Vision-Allrounder |
|
||||
| `heavy` | gpt-oss-120b (mxfp4) | 600 | 32k ctx; Chef-Gutachter (nur nachts / gezielter On-Demand Call) |
|
||||
| `dense-planer` | Qwen3.8-27B (Q4_K_M, BF16 mmproj) | 300 | 65k ctx; Dichtes 27B-Modell für tiefes Planen & Multimodalität |
|
||||
| `reranker` | Qwen3-Reranker-0.6B (q8_0, Mungert) | 0 | Gedächtnis-Sortierer (`/v1/rerank`) |
|
||||
| `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Chroma / Mem0 |
|
||||
| `hermes` + `fast` | Qwen3.6-35B-A3B **Uncensored** (HauhauCS Aggressive, Q4_K_M, giocom-DFlash) — seit 17.09.2026 | 0 | Agent-Hirn. `-c 131072 --parallel 2` → **65536/Slot**, KV **q8_0**, ~94–101 t/s (Prüfstand 17.09.). Original (MTP-GGUF UD-Q4_K_M) liegt daneben, Rückweg = Pfad |
|
||||
| `coder` | Qwen3.8-27B **Uncensored** (JonathanColetti/Heretic, Q4_K_M, eigene mmproj F16) — seit 17.09.2026 | 5400 | **131072 ctx** — `--parallel 1`, der Coder bekommt den ganzen Slot (34a9862). Multimodal, **~32 t/s mit DFlash2-Draft** (17.09.), ohne Draft 12,7 (dicht = bandbreitengebunden) |
|
||||
| `debugger` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65536 ctx; Runtime-Diagnostik & Fehler-Debugger (multimodal) |
|
||||
| `vision` | Qwen3-VL-30B-A3B-Instruct (Q4_K_M) | 900 | 32768 ctx; Multimodal-Augen (On-Demand) |
|
||||
| `heavy` | **= Qwen3.8-27B** (zweiter Alias des Coders, seit 04.09.) | 5400 | Planen/Review (OpenChamber) + chat-Lane des Gateways. gpt-oss-120b (60 GB, AA-Index 24 vs 52) liegt ohne Rolle als Rollback bereit, direkt als `gpt-oss-120b` ansprechbar |
|
||||
| `reranker` | Qwen3-Reranker-0.6B (q8_0, Mungert) | 0 | Sortiert Suchtreffer nach echter Relevanz (`/v1/rerank`) |
|
||||
| `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Suche/Sortierung |
|
||||
|
||||
> **Diese sieben sind ALLES** — abgeglichen mit `/etc/llama-swap/config.yaml` am 27.08.2026 (Pfade 17.09.2026).
|
||||
>
|
||||
> **Prüfstand 17.09.2026** (`deploy/bench/pruefstand-hirn.py`; Kandidat auf `:5899`, Hermes spricht per `--provider pruefstand`
|
||||
> aus `providers:` der Hermes-Config dagegen — Live-Stack bleibt unberührt): Uncensored-3.6 = 100,8 t/s @13k (Original 92,8),
|
||||
> Tools 11/12, Hermes-Smoke grün · Uncensored-27B = 30,4 t/s @13k (Original 26,2), Tools 6/6, Coding 3/3 · **Nemotron 3.5 Lightning
|
||||
> 30B-A3B** (Q4_0 + MTP-Draft, `/srv/models/Nemotron-3.5-Lightning-30B-A3B-GGUF/`) = Hirn-Klasse: 10,5 s Prefill @13k, 91,8 t/s,
|
||||
> Tools 3/3, Hermes grün — liegt als Reserve-Kandidat bereit, nicht besetzt. ‼ **Bild + DFlash2 = HTTP 500** („failed to process
|
||||
> speculative batch") beim Coder — Vorbestand seit 04.09., unabhängig vom Modell; Bilder laufen über die Gateway-Bildweiche zu `vision`.
|
||||
> Frühere Tabellen führten zusätzlich `kritiker` (Devstral-Small-2-24B), `scout` (GLM-4.6V-Flash),
|
||||
> `dense-planer` und `doctor`. Die gibt es nicht mehr: die Modell-Konsolidierung (`9a35be9`, 19.08.)
|
||||
> warf sie aus llama-swap, und `fremdblick.sh` — der einzige Aufrufer des Kritikers — fiel mit dem
|
||||
> MC2-Kahlschlag (`1e68f62`). Gewollt so: **ein Coder, ein Agent-Hirn.** `kritiker` und `scout` stehen
|
||||
> weiterhin in `ROLE_IDS` (backend/services/llamaswap.py) — das ist die Liste besetzbarer Rollen,
|
||||
> keine Zusage, dass sie besetzt sind.
|
||||
|
||||
Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben).
|
||||
|
||||
## Hermes (Agent)
|
||||
|
||||
- **v0.20.4** (git-Install `~/.hermes/hermes-agent`), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
||||
- **Bot Mode:** Unterstützt Multi-Bot-Roster (`Lucy` als Haupt-Agentin, `Coder` mit `Qwen3-Coder-Next`, `Debugger` mit `Muse-Glimmer-30B`).
|
||||
- `approvals.mode: smart`, `approvals.cron_mode: deny`.
|
||||
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-memory, mission-control-stack, hermes-pc-control, hermes-web-fetch, mcp_voice.
|
||||
- **v0.21.3** (2026.9.14; git-Install `~/.hermes/hermes-agent`, main @ dd13b475 vom 17.09.2026), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
||||
- **Bot-Roster: leer** (live geprüft 27.08.2026). Hermes *kann* mehrere Bots, genutzt wird es nicht —
|
||||
die Profile fahren `hermes`, `fast` und `vision`. Frühere Fassungen behaupteten hier einen Roster
|
||||
mit `Coder`/`Qwen3-Coder-Next` und `Debugger`; beides trifft nicht zu.
|
||||
- `approvals.mode: 'off'`, `approvals.cron_mode: auto` — **User-Entscheid** (so seit spätestens dem KISS-Umbau
|
||||
21.08.2026, bestätigt 17.09.2026: „approvals bleibt off"). Lucy fragt nicht nach Freigaben; die Grenze ziehen
|
||||
ufw, PC-Executor-Token und command_allowlist (siehe Security). Frühere Fassungen nannten `smart`/`deny` (Stand 03.07.).
|
||||
- Config-Schema **v45** (migriert 17.09.2026): `connections`-Toolset (Nous-Konnektoren, `manage_connections`) bewusst in
|
||||
`agent.disabled_toolsets`; Curator archiviert ungenutzte Skills nach 30 Tagen (`skills/.archive/`, `hermes curator restore`).
|
||||
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-stack, hermes-pc-control, hermes-web-fetch, mcp_voice.
|
||||
(`mission-control-memory` ist am 27.08.2026 entfallen — Hermes führt sein Gedächtnis selbst.)
|
||||
|
||||
## Automatik-Fahrplan (was nachts von allein läuft)
|
||||
|
||||
@@ -89,10 +109,12 @@ Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben).
|
||||
| 10 9, 16.08. | **Gedaechtnis-Check** (einmalig: Gedächtnis-Schutt räumen, alte Einträge bereinigen) |
|
||||
| So 04:30 | **Auto-Update** Router→Engine→Hermes (Fangnetz: Backup→Postcheck→Auto-Rollback+Pin) |
|
||||
|
||||
**`mc2-autoupdate.timer` ist AN (User-Entscheid 10.07.,** ersetzt „nur bestätigt" vom 04.07.):
|
||||
Fremd-Software-Updates (Router/Engine/Hermes) dürfen automatisch laufen; der User klickt sie
|
||||
zusätzlich oft manuell im Wartungs-Drawer. deploy.sh enablet den Timer bewusst NICHT selbst —
|
||||
der Schaltzustand wird nur von Hand geändert. Manueller Lauf: `bash deploy/autoupdate.sh`.
|
||||
**Sonntags-Update = Hermes-Cron `0ec52231783b`** (`~/.hermes/scripts/sonntags-update.sh` →
|
||||
`deploy/autoupdate.sh`, seit dem KISS-Umbau 21.08.); `mc2-autoupdate.timer` ist disabled — gewollt.
|
||||
Fremd-Software-Updates (Router/Engine/Hermes) dürfen automatisch laufen (User-Entscheid 10.07.); der
|
||||
User klickt sie zusätzlich oft manuell im Wartungs-Drawer. Rot ⇒ Rollback + Pin in
|
||||
`/srv/models/mc2-pins.json`; gepinnte Ebenen werden **still übersprungen**, bis der Pin von Hand
|
||||
gelöst wird (06.–17.09.2026 stand die Box so zwei Wochen still). Manueller Lauf: `bash deploy/autoupdate.sh`.
|
||||
OS-Sicherheitsupdates laufen separat via unattended-upgrades.
|
||||
|
||||
## Security (Stufe 0, live)
|
||||
@@ -101,15 +123,17 @@ PC-Executor nur mit Bearer `HERMES_PC_TOKEN` (fail-closed) · ufw default deny,
|
||||
LAN→22/9001/8080 (+Alt-Port 7681) · Prompt-Injection-Guard `mcp/guard.py` (wrappt
|
||||
Web-/Bildschirm-Text) · mc2-memory lernt nicht aus untrusted Turns · command_allowlist =
|
||||
5 exakt gepinnte curls (fnmatch; Shell-Operatoren hart geblockt — NIE offenes `curl *`) ·
|
||||
Box-sudo braucht Passwort; NOPASSWD nur für update-engine/swap (`/etc/sudoers.d/mc2-autonomie`).
|
||||
Box-sudo ist **NOPASSWD: ALL** (live `sudo -n -l`, 04.09. und 17.09.2026); die alte Whitelist in
|
||||
`/etc/sudoers.d/mc2-autonomie` (update-engine/swap, Dienst-Restarts, journalctl) steht daneben weiter.
|
||||
**Security-Config (approvals/Tokens/ufw) NIE ohne explizites User-Ja ändern.**
|
||||
|
||||
## Deploy & Pipeline
|
||||
|
||||
- **Standard-Weg für Box-Arbeit = Auftragsbuch:** Vorschlags-Branch auf Gitea
|
||||
(`wartung/*`, `orchestrator/*`, `doku/*`, `feature/*`) → Karte in der UI → User-Klick =
|
||||
Merge + Deploy + Health + Auto-Revert (detached Runner).
|
||||
- Manuell: main auf Gitea pushen → `ssh … 'bash ~/mission-control-v2/deploy/deploy.sh'`
|
||||
- **Standard-Weg für Box-Arbeit (seit 04.09.2026 der EINZIGE):** Vorschlags-Branch auf Gitea
|
||||
(`wartung/*`, `doku/*`, `feature/*`) → Ampel-CI grün → **der User merged auf main** →
|
||||
`deploy.sh` auf der Box. Das Auftragsbuch (Karten-Klick, detached Annahme-Runner, Bagatell-
|
||||
Annahme, Lucy-Annahme über den PC-Executor) ist ausgebaut — es war seit 02.08. ungenutzt.
|
||||
- Deploy: main auf Gitea pushen → `ssh … 'bash ~/mission-control-v2/deploy/deploy.sh'`
|
||||
ODER `POST :9001/api/system/self-update` (restart meldet ok:false — harmlos, /api/health prüfen).
|
||||
- deploy.sh macht `git reset --hard origin/main` (main MUSS vorher auf Gitea liegen) und baut
|
||||
KEIN Frontend → dist lokal bauen + committen. Fallen: [FALLEN.md](FALLEN.md).
|
||||
|
||||
@@ -84,8 +84,10 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
|
||||
- **Zed ACP / Hermes-im-Editor: NICHT machen.** Editor-Agent + Box-coder-Modell + AGENTS.md
|
||||
ist der Sweet Spot; Hermes dazwischen = 70-KB-Prompt-Overhead. (Zed selbst ist inzwischen
|
||||
vom PC entfernt — Hermes Desktop übernahm.)
|
||||
- **`approvals.cron_mode: deny` bleibt** (Autonomie-Härtung 1c, User wählte Option A):
|
||||
Gefahr-Befehle + execute_code sind im Cron-/Autonom-Kontext geblockt, interaktiv `smart`.
|
||||
- **Approvals bleiben AUS** (`approvals.mode: 'off'`, `cron_mode: auto`; so seit spätestens dem KISS-Umbau
|
||||
21.08.2026, vom User am 17.09.2026 bestätigt). Die Schutzlinie liegt außen — ufw, PC-Executor-Token,
|
||||
command_allowlist — nicht in der Freigabe-Frage. *Überholt damit:* „`approvals.cron_mode: deny` bleibt"
|
||||
(Autonomie-Härtung 1c, Option A: Gefahr-Befehle + execute_code im Cron-Kontext geblockt, interaktiv `smart`).
|
||||
- **Fremd-Kritik via `fremdblick.sh`** (Ein-Schuss an leichtes Fremdmodell), NIE via
|
||||
delegate_task→heavy (64k-Floor + Lade-Tod). Raster: prose=GLM-4.7-Flash (kritiker),
|
||||
code=Coder-Next. Harte Regel: **REPRODUZIERT-ODER-ABGELEHNT** — Freispruch nur mit
|
||||
|
||||
@@ -32,6 +32,14 @@ planen, **jede Session hinterlässt einen sauberen Stand**.
|
||||
Antigravity liest den F:\-Checkout — versioniert + deploybar). Der Vault bleibt Lucys
|
||||
Lernschicht.
|
||||
|
||||
8. **Lucy = innere Stimme, kein Körper (04.09.2026, ergänzt Punkt 2).** Raphael-Umbau: Avatar,
|
||||
Loft, Overlay, Mimik, Dazwischenrufe raus; ein HUD am PC, Telegram unterwegs, **eine Stimme**
|
||||
für beide (`lucy-stimme.service` auf der Box). Kein zweites Hirn auf dem PC. Details, Reihenfolge
|
||||
der Annahme und der SOUL.md-Vorschlag: [RAPHAEL.md](RAPHAEL.md).
|
||||
9. **Auftragsbuch ausgebaut (04.09.2026).** Die Queue→Karte→Klick-Mechanik aus Punkt 2/3/5 war
|
||||
seit 02.08. ungenutzt (Kanban-Tools seit 21.08. aus). Gate ist jetzt: Ampel grün + Merge durch
|
||||
den User + `deploy.sh`. Bagatell-Annahme ohne Klick (Punkt 5) entfällt damit ebenfalls.
|
||||
|
||||
## Recherche-Fundament (10.07., Web)
|
||||
|
||||
Das Industrie-Muster 2026 ist exakt unsere Architektur: Issue → Agent → PR → Mensch-Gate, nie
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 110,
|
||||
"trailingComma": "all",
|
||||
"arrowParens": "always"
|
||||
}
|
||||
-6
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36
File diff suppressed because one or more lines are too long
-31
File diff suppressed because one or more lines are too long
+51
File diff suppressed because one or more lines are too long
-26
File diff suppressed because one or more lines are too long
+16
File diff suppressed because one or more lines are too long
+36
File diff suppressed because one or more lines are too long
-36
File diff suppressed because one or more lines are too long
+40
File diff suppressed because one or more lines are too long
-35
File diff suppressed because one or more lines are too long
+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-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};
|
||||
import{u,ae as m,k as x,j as s,y as n,af as b,b as p,n as f,q as h}from"./index-DMY9lUCb.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};
|
||||
@@ -0,0 +1 @@
|
||||
import{O as h,u as g,P as p,r as d,j as e,b as x,T as j,S as N,n as v,U as m,V as w,q as f}from"./index-DMY9lUCb.js";import{E as k}from"./external-link-DcJao8h4.js";import{R as y}from"./refresh-cw-hAcZthO8.js";function K(){const{data:t}=h(),u=g(),a=t!=null&&t.box_console_url?p(t.box_console_url):void 0,n=t==null?void 0:t.box_console_reachable,[o,i]=d.useState(!1),[c,l]=d.useState("");async function b(){i(!0),l("");try{const s=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})}),r=s.ok?"Konsolen-Dienst neu gestartet — einen Moment, dann lädt das Terminal.":`Neustart fehlgeschlagen: ${s.err||"Unbekannter Fehler"}`;l(r),m(s.ok?"erfolg":"fehler",r),w(u,f.agentStatus,f.services)}catch(s){const r=`Neustart fehlgeschlagen: ${(s==null?void 0:s.message)||s}`;l(r),m("fehler",r)}finally{i(!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:x("h-2 w-2 rounded-full",n?"bg-emerald-500 animate-pulse":"bg-amber-500")}),n?"online":"offline"]}),a&&e.jsxs("a",{href:a,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(k,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),a?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:[n===!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(j,{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:b,disabled:o,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(y,{className:x("h-3.5 w-3.5",o&&"animate-spin")})," Dienst neu starten"]}),c&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:c})]}),e.jsx("iframe",{src:a,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{K as KonsoleView};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user