Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,11 +1,5 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"mc2-memory": {
|
|
||||||
"command": "python",
|
|
||||||
"args": [
|
|
||||||
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mc2-system": {
|
"mc2-system": {
|
||||||
"command": "python",
|
"command": "python",
|
||||||
"args": [
|
"args": [
|
||||||
|
|||||||
+55
-37
@@ -1,37 +1,55 @@
|
|||||||
{
|
{
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "mc2",
|
"name": "mc2",
|
||||||
"runtimeExecutable": "F:\\Coding Stuff\\mission-control-2\\backend\\.venv\\Scripts\\python.exe",
|
"runtimeExecutable": "F:\\Coding Stuff\\mission-control-2\\backend\\.venv\\Scripts\\python.exe",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"-m",
|
"-m",
|
||||||
"uvicorn",
|
"uvicorn",
|
||||||
"app:app",
|
"app:app",
|
||||||
"--app-dir",
|
"--app-dir",
|
||||||
"F:\\Coding Stuff\\mission-control-2\\backend",
|
"F:\\Coding Stuff\\mission-control-2\\backend",
|
||||||
"--port",
|
"--port",
|
||||||
"9000"
|
"9000"
|
||||||
],
|
],
|
||||||
"port": 9000
|
"port": 9000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5180", "--strictPort"],
|
"runtimeArgs": [
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"run",
|
||||||
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
|
"dev",
|
||||||
"autoPort": false,
|
"--",
|
||||||
"port": 5180
|
"--port",
|
||||||
},
|
"5180",
|
||||||
{
|
"--strictPort"
|
||||||
"name": "frontend-mock",
|
],
|
||||||
"runtimeExecutable": "npm",
|
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
|
"env": {
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"MC_API_TARGET": "http://192.168.178.151:9001"
|
||||||
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
|
},
|
||||||
"autoPort": false,
|
"autoPort": false,
|
||||||
"port": 5181
|
"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
|
# 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) +
|
# 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
|
name: Ampel
|
||||||
on: [push, pull_request]
|
on: [push, pull_request]
|
||||||
|
|
||||||
@@ -42,7 +43,65 @@ jobs:
|
|||||||
rot=1
|
rot=1
|
||||||
else
|
else
|
||||||
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
( 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
|
if [ $rot -eq 0 ]; then
|
||||||
echo "== Frontend: Push nach release-dist =="
|
echo "== Frontend: Push nach release-dist =="
|
||||||
git config --global user.name "Gitea Actions"
|
git config --global user.name "Gitea Actions"
|
||||||
|
|||||||
+19
-24
@@ -1,24 +1,19 @@
|
|||||||
# Python
|
# Python
|
||||||
backend/.venv/
|
backend/.venv/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# Node / Vite
|
# Node / Vite
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
# frontend/dist wird committet (kein Node-Build auf der Box) — siehe deploy/
|
# 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.
|
# Env / local
|
||||||
# Wird per Direkt-Deploy auf die Box gespielt (dist/avatar.vrm), nicht über git.
|
*.env
|
||||||
frontend/public/avatar.vrm
|
.DS_Store
|
||||||
frontend/dist/avatar.vrm
|
|
||||||
|
# Box-Recon-/Scratch-Skripte (lokale Diagnose, nicht fürs Repo)
|
||||||
# Env / local
|
box_recon*
|
||||||
*.env
|
gemma_swap*
|
||||||
.DS_Store
|
|
||||||
|
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||||
# Box-Recon-/Scratch-Skripte (lokale Diagnose, nicht fürs Repo)
|
frontend/tsconfig.tsbuildinfo
|
||||||
box_recon*
|
|
||||||
gemma_swap*
|
|
||||||
|
|
||||||
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
|
||||||
frontend/tsconfig.tsbuildinfo
|
|
||||||
|
|||||||
@@ -1,60 +1,67 @@
|
|||||||
# AGENTS.md — Mission Control 2.0
|
# AGENTS.md — Mission Control 2.0
|
||||||
|
|
||||||
Projekt-Geschmack für Coding-Agenten (Kilo Code, Claude Code lesen diese Datei).
|
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/`.
|
Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `README.md`, `docs/`.
|
||||||
|
|
||||||
## Was das ist
|
## Was das ist
|
||||||
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
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) ·
|
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.
|
**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):**
|
**Hardware-Spezifikationen der Box (WICHTIG für alle Agenten):**
|
||||||
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||||
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
- **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.
|
- **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.
|
**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)
|
## Sprache (nicht verhandelbar)
|
||||||
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
- **Alles User-Sichtbare ist Deutsch:** UI-Texte, Fehlermeldungen, Update-/Radar-Meldungen,
|
||||||
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
|
Skills, LLM-Summaries, Commit-Messages, Code-Kommentare.
|
||||||
- **Hermes' INTERNE System-Prompts bleiben Englisch** (fremde Software, wir forken sie nicht).
|
- **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.
|
Antwort-Sprache ≠ Prompt-Sprache — Deutsch kommt aus SOUL.md, nicht aus den Tool-Prompts.
|
||||||
|
|
||||||
## Build & Deploy (die häufigste Falle)
|
## Build & Deploy (die häufigste Falle)
|
||||||
- **`frontend/dist` WIRD committet.** Auf der Box läuft KEIN Node-Build; das Backend liefert die
|
- **`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
|
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.
|
**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
|
||||||
- **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).
|
||||||
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.
|
||||||
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
|
||||||
|
## Agentic IDE & Vibe Coding
|
||||||
## Agentic IDE & Vibe Coding
|
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
|
||||||
- **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.
|
||||||
- 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.
|
||||||
- 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.
|
||||||
- **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
|
||||||
## Zeit & Umgebung
|
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
||||||
- **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
|
||||||
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
(siehe `backend/services/reminders.py`).
|
||||||
(siehe `backend/services/reminders.py`).
|
|
||||||
|
## Backend-Konventionen
|
||||||
## Backend-Konventionen
|
- Router unter `backend/routers/` (`APIRouter(prefix="/api")`), Logik in `backend/services/`
|
||||||
- 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
|
||||||
(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`);
|
||||||
`services.maintenance` (System-Dienste via `sudo -n`, User-Dienste via `systemctl --user`);
|
keine zweite Allowlist in einem Router duplizieren.
|
||||||
keine zweite Allowlist in einem Router duplizieren.
|
- **Gate vor Commit:** `python -m py_compile <geänderte .py>` muss durchlaufen.
|
||||||
- **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 =
|
||||||
- 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.
|
||||||
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
|
||||||
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
`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,
|
||||||
## Grenzen (Verdikt, eingehalten)
|
was gebraucht *würde*, schränken aber nichts ein. Das ist eine bewusste Entscheidung für die
|
||||||
- **Hermes-Quellcode nie selbst patchen** (Fork verboten). Config/Deps ja, Code-Umbau nein.
|
Single-User-Appliance; verlasse dich beim Bauen nicht darauf, dass eine Whitelist dich bremst.
|
||||||
- **Security-Config** (approvals, Tokens, ufw, command_allowlist) **nie ohne explizites User-Ja.**
|
Wer das wirklich härten will, kommt um einen eigenen Service-User nicht herum — die Pauschalzeile
|
||||||
- Alles reversibel halten: Branch + Backup + Pin.
|
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-*`.)
|
||||||
## Gitea
|
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
||||||
Remote = `Hitonabi/mission-control-v2`. Auth ist flatterhaft → **Push mit Retry**, nur über
|
|
||||||
PowerShell/GCM. Neue Repos per API anlegen. Kein GitHub.
|
## 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.
|
||||||
|
|||||||
@@ -49,29 +49,33 @@ Wächtern geboren**:
|
|||||||
|
|
||||||
## Modelle & Rollen
|
## 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 |
|
| 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) |
|
| `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 |
|
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6–90 t/s** | Lucys Hirn **und** Sucher-Subagent. Immer warm, 65k/Slot |
|
||||||
| `debugger` / `doctor` | Muse-Glimmer-30B | **40–50 t/s** (DFlash) | Runtime-Debugger & Fehler-Diagnostiker (Multimodal) |
|
| `debugger` | 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) |
|
|
||||||
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen (On-Demand) |
|
| `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 |
|
| `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 | Vektorsuche + Feinsortierung |
|
||||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
|
||||||
|
|
||||||
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
> Sieben Rollen, mehr nicht. Frühere Fassungen listeten hier auch `kritiker`, `scout` und
|
||||||
auf dieser bandbreitenbegrenzten Box bricht sein Prefill mit wachsendem Kontext ein (bei 32k
|
> `dense-planer` — die Modell-Konsolidierung vom 19.08. hat sie entfernt (ein Coder, ein
|
||||||
gemessene 63 t/s ≈ **9 Minuten nur zum Lesen**). Mit dem 16k-Deckel bleibt der schlimmste Fall
|
> Agent-Hirn). Der dichte 27B ist seither nicht mehr „Planer" neben dem Coder, sondern **ist**
|
||||||
je Review unter einer Minute. Deshalb ist er der **Gegenleser für Etappen**, nicht der Coder —
|
> der Coder. Details: `docs/wissen/STACK.md`.
|
||||||
als Coder ist er auf dieser Box disqualifiziert (siehe VERDIKTE).
|
|
||||||
|
‼️ **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
|
‼️ **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
|
(`groups.brains`) steht auf **`persistent: true`** (Verdrängungsschutz; live gegengeprüft
|
||||||
llama-swap vor einem großen Modell nicht ab und der Kernel schießt Prozesse ab. Details und
|
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).
|
Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
|
||||||
|
|
||||||
## Qualitäts-Tore
|
## Qualitäts-Tore
|
||||||
|
|||||||
+17
-11
@@ -19,13 +19,9 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
|||||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||||
# Geteiltes Gedächtnis (SQLite, WAL). Persistent neben den Modellen.
|
# Gedächtnis: MC2 hält KEINS mehr. Bis August 2026 lief hier erst eine eigene SQLite-DB,
|
||||||
# Hinweis: nur noch für die einmalige Mem0-Migration relevant — das aktive Gedächtnis
|
# dann ein semantischer Sidecar auf :8765 — beides ist abgelöst, der Port ist tot.
|
||||||
# liegt jetzt in Mem0/Chroma hinter dem Sidecar (siehe MEM0_SERVICE_URL).
|
# Einzige Gedächtnis-Wahrheit ist jetzt Hermes selbst (HERMES_API_URL, siehe unten).
|
||||||
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db")))
|
|
||||||
# Mem0-Sidecar (auto-lernendes, semantisches Gedächtnis). Läuft im ~/.mem0/venv (Python 3.12),
|
|
||||||
# weil mem0+chromadb unter dem 3.14-Backend nicht laufen. MC2 spricht ihn lokal per HTTP an.
|
|
||||||
MEM0_SERVICE_URL = os.environ.get("MC_MEM0_SERVICE_URL", "http://127.0.0.1:8765").rstrip("/")
|
|
||||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||||
@@ -72,7 +68,7 @@ V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
|||||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||||
# (Tools + geteiltes Mem0) wie CLI/Telegram, nur über HTTP.
|
# (Tools + eigenes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||||
def _read_hermes_env(key: str) -> str:
|
def _read_hermes_env(key: str) -> str:
|
||||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||||
@@ -97,8 +93,14 @@ HERMES_API_KEY = (
|
|||||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||||
|
|
||||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||||
# Eigenes Python-3.12-venv (~/.voice/venv), analog Mem0-Sidecar. MC2 proxyt nach außen.
|
# Eigenes Python-3.12-venv (~/.voice/venv), 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("/")
|
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
|
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||||
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
|
# rückwärts geproxyt (routers/console.py → same-origin /console/). So braucht die Konsole
|
||||||
@@ -108,8 +110,12 @@ BOX_CONSOLE_UPSTREAM = os.environ.get("MC_BOX_CONSOLE_UPSTREAM", "http://127.0.0
|
|||||||
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
# Öffentlicher, gleicher-Ursprung-Pfad, unter dem MC2 die Konsole ausliefert (iframe-Ziel).
|
||||||
BOX_CONSOLE_PATH = "/console/"
|
BOX_CONSOLE_PATH = "/console/"
|
||||||
# Eingebaute Hermes-Web-GUI (`hermes serve`/`dashboard` — die richtige Agent-Oberfläche mit
|
# 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
|
# Threads/Tool-Calls, die auch die Electron-Desktop-App umhüllt). ‼️ Sie bindet NICHT auf Loopback:
|
||||||
# (127.0.0.1:9119, kein Login — LAN-Trust wie Terminal/Konsole) und wird von MC2 same-origin unter
|
# 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
|
# /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
|
# 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.
|
# liegen; der Proxy streift /hermes-ui ab. Kein eigener Firewall-Port nötig.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from services import agent_aktivitaet
|
||||||
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
from services.agent import agent_status, hermes_brain_info, set_agent_brain, update_brain_model
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
@@ -39,3 +40,13 @@ def set_brain(body: SetBrainReq) -> dict:
|
|||||||
def set_brain_model(body: BrainReq) -> dict:
|
def set_brain_model(body: BrainReq) -> dict:
|
||||||
ok = update_brain_model(body.model)
|
ok = update_brain_model(body.model)
|
||||||
return {"ok": ok}
|
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,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 fastapi import APIRouter
|
||||||
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
from services.connect import DEFAULT_HOST, build_snippets, check_health
|
||||||
@@ -7,14 +7,11 @@ router = APIRouter(prefix="/api")
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/connect")
|
@router.get("/connect")
|
||||||
def connect(host: str = DEFAULT_HOST, mcp_path: str | None = None) -> dict:
|
def connect(host: str = DEFAULT_HOST) -> dict:
|
||||||
kwargs = {}
|
return build_snippets(host=host)
|
||||||
if mcp_path:
|
|
||||||
kwargs["mcp_script_path"] = mcp_path
|
|
||||||
return build_snippets(host=host, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/connect/health")
|
@router.get("/connect/health")
|
||||||
def connect_health() -> dict:
|
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()
|
return check_health()
|
||||||
|
|||||||
+85
-37
@@ -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
|
Zwei Endpunkte, ein Sammler:
|
||||||
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.
|
|
||||||
|
|
||||||
Quellen: Briefkasten/Chronik (in-process-Cursor), Ideen-Queue ((id,status)-Paare),
|
GET /api/stream (v3-Umbau P4, 28.08.2026) — der aktuelle Kanal. Zwei Ereignisarten:
|
||||||
Auftragsbuch + Erinnerungen (Datei-mtimes), geladene Modelle (Running-Set — Idee aus
|
· `invalidate` Nur bei Änderung, mit den betroffenen React-Query-Schlüsseln.
|
||||||
der Werkstatt-Karte feature/sse-backend-v1). BEWUSST NICHT dabei: System-/Token-
|
· `metrik` Jede Sekunde ein Messpunkt (CPU/RAM/GPU/Temp/Token-Zähler).
|
||||||
Metriken (ändern sich jede Sekunde — da ist Polling das richtige Werkzeug und ein
|
|
||||||
invalidate-Event nur Lärm).
|
|
||||||
|
|
||||||
Versöhnt 15.07. abends: Die angenommene Werkstatt-Version nutzte `type:` statt
|
GET /api/events — der alte Kanal, nur `invalidate`. Bleibt EINE Fassung lang stehen,
|
||||||
`event:` (ungültiges SSE-Framing → EventSource-Listener feuert NIE), einen globalen
|
weil ein Browser-Tab nach einem Deploy noch das vorige Bündel halten kann und dieses
|
||||||
Snapshot über alle Clients und einen nicht existierenden Ideen-Endpunkt — Kern
|
nur `/api/events` kennt. Danach entfernen.
|
||||||
wieder die getestete Hand-Implementierung (E2E: Announce → invalidate binnen
|
|
||||||
Sekunden), Modell-Quelle aus der Karte übernommen.
|
|
||||||
|
|
||||||
Frontend-Gegenstück: frontend/src/lib/events.ts (EventSource, invalidiert die
|
WARUM METRIKEN JETZT MITKOMMEN: Bis P4 pollte das Frontend `/api/system/status` und
|
||||||
Caches, entspannt die Fallback-Poller ×5; reißt der Strom, reconnectet EventSource
|
`/api/system/token-stats` im 3-Sekunden-Takt — zwei Dauer-Anfragen, unabhängig davon, ob
|
||||||
selbst und bis dahin pollt die UI wie bisher).
|
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
|
import asyncio
|
||||||
@@ -35,7 +40,8 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
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
|
KEEPALIVE_S = 20.0 # Kommentar-Ping, damit Proxies/Browser die Verbindung halten
|
||||||
|
|
||||||
|
|
||||||
@@ -66,6 +72,13 @@ def _fingerprints() -> dict[str, object]:
|
|||||||
fp["models"] = json.dumps(sorted(str(m) for m in llamaswap.get_running_models()))
|
fp["models"] = json.dumps(sorted(str(m) for m in llamaswap.get_running_models()))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
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
|
||||||
# Auftragsbuch (Annahme-Status + Karten-Meldungen) & Erinnerungen: Datei-mtimes
|
# Auftragsbuch (Annahme-Status + Karten-Meldungen) & Erinnerungen: Datei-mtimes
|
||||||
fp["auftragsbuch"] = (_mtime(MODELS_DIR / "mc2-auftragsbuch.json"),
|
fp["auftragsbuch"] = (_mtime(MODELS_DIR / "mc2-auftragsbuch.json"),
|
||||||
_mtime(MODELS_DIR / "mc2-announce-branches.json"))
|
_mtime(MODELS_DIR / "mc2-announce-branches.json"))
|
||||||
@@ -73,28 +86,63 @@ def _fingerprints() -> dict[str, object]:
|
|||||||
return fp
|
return fp
|
||||||
|
|
||||||
|
|
||||||
@router.get("/events")
|
async def _strom(request: Request, mit_metrik: bool):
|
||||||
async def events(request: Request) -> StreamingResponse:
|
"""Gemeinsamer Kern beider Endpunkte.
|
||||||
async def strom():
|
|
||||||
# Basislinie JE VERBINDUNG (der Client hat beim Verbinden frisch geladen) —
|
Die Basislinie entsteht JE VERBINDUNG (der Client hat beim Verbinden frisch geladen) —
|
||||||
# ein globaler Snapshot würde bei mehreren Clients Events verschlucken.
|
ein globaler Snapshot würde bei mehreren Clients Events verschlucken.
|
||||||
alt = _fingerprints()
|
"""
|
||||||
yield ": verbunden\n\n"
|
alt = _fingerprints()
|
||||||
seit_ping = 0.0
|
yield ": verbunden\n\n"
|
||||||
while True:
|
|
||||||
if await request.is_disconnected():
|
seit_abdruck = 0.0
|
||||||
return
|
seit_ping = 0.0
|
||||||
await asyncio.sleep(TICK_S)
|
takt = METRIK_S if mit_metrik else ABDRUCK_S
|
||||||
seit_ping += TICK_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()
|
neu = _fingerprints()
|
||||||
keys = [k for k, v in neu.items() if k in alt and v != alt[k]]
|
keys = [k for k, v in neu.items() if k in alt and v != alt[k]]
|
||||||
alt.update(neu)
|
alt.update(neu)
|
||||||
if keys:
|
if keys:
|
||||||
yield f"event: invalidate\ndata: {json.dumps({'keys': keys})}\n\n"
|
yield f"event: invalidate\ndata: {json.dumps({'keys': keys})}\n\n"
|
||||||
seit_ping = 0.0
|
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",
|
if seit_ping >= KEEPALIVE_S:
|
||||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
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.")
|
"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
|
# Bild-Beschreibung für Coder-Ziele: Prompt bewusst auf wörtliche Wiedergabe von
|
||||||
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
# Code/Fehlermeldungen getrimmt — der Coder arbeitet nur mit diesem Text weiter.
|
||||||
_BILD_BESCHREIB_PROMPT = os.environ.get(
|
_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"]):
|
for i, part in enumerate(m["content"]):
|
||||||
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
if isinstance(part, dict) and part.get("type") in IMAGE_PART_TYPES:
|
||||||
fundstellen.append((m, i, part))
|
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):
|
for nr, (msg, idx, part) in enumerate(fundstellen, start=1):
|
||||||
frage = {
|
frage = {
|
||||||
"model": vision_alias,
|
"model": vision_alias,
|
||||||
@@ -99,7 +110,7 @@ async def _bilder_fuer_coder_beschreiben(body: dict, client, vision_alias: str)
|
|||||||
text = ""
|
text = ""
|
||||||
for versuch in (1, 2):
|
for versuch in (1, 2):
|
||||||
try:
|
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)
|
r = await c.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json=frage)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
text = ((r.json().get("choices") or [{}])[0].get("message") or {}).get("content") or ""
|
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):
|
async def models(request: Request):
|
||||||
client = request.app.state.gw_client # geteilter Keep-Alive-Client (siehe app.py lifespan)
|
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)
|
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"
|
# 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
|
# 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.
|
# 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):
|
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
|
_apply_no_think_marker(body) # Lucy-Voice-Schnellspur: Marker strippen + Thinking aus
|
||||||
requested = str(body.get("model") or "auto")
|
requested = str(body.get("model") or "auto")
|
||||||
if requested.lower() in ("auto", "chat", "coding"):
|
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 pydantic import BaseModel
|
||||||
from services import maintenance
|
from services import geheimnisse, maintenance
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
class SudoReq(BaseModel):
|
|
||||||
sudo_password: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class RestartReq(BaseModel):
|
class RestartReq(BaseModel):
|
||||||
service: str
|
service: str
|
||||||
sudo_password: str | None = None
|
|
||||||
|
|
||||||
|
class GeheimnisReq(BaseModel):
|
||||||
|
schluessel: str
|
||||||
|
wert: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/maintenance/updates")
|
@router.get("/maintenance/updates")
|
||||||
@@ -28,32 +37,32 @@ def update_details(kind: str) -> dict:
|
|||||||
return maintenance.update_details(kind)
|
return maintenance.update_details(kind)
|
||||||
|
|
||||||
@router.post("/maintenance/check-updates")
|
@router.post("/maintenance/check-updates")
|
||||||
def check_updates(body: SudoReq) -> dict:
|
def check_updates() -> dict:
|
||||||
res = maintenance.check_updates_job(body.sudo_password)
|
res = maintenance.check_updates_job()
|
||||||
if isinstance(res, dict) and not res.get("ok", True):
|
if isinstance(res, dict) and not res.get("ok", True):
|
||||||
return res
|
return res
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/os-update")
|
@router.post("/maintenance/os-update")
|
||||||
def os_update(body: SudoReq) -> dict:
|
def os_update() -> dict:
|
||||||
res = maintenance.os_update_job(body.sudo_password)
|
res = maintenance.os_update_job()
|
||||||
if isinstance(res, dict) and not res.get("ok", True):
|
if isinstance(res, dict) and not res.get("ok", True):
|
||||||
return res
|
return res
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/engine-update")
|
@router.post("/maintenance/engine-update")
|
||||||
def engine_update(body: SudoReq) -> dict:
|
def engine_update() -> dict:
|
||||||
res = maintenance.engine_update_job(body.sudo_password)
|
res = maintenance.engine_update_job()
|
||||||
if not res:
|
if not res:
|
||||||
raise HTTPException(400, "Kein Engine-Update-Befehl gesetzt (MC_ENGINE_UPDATE_CMD).")
|
raise HTTPException(400, "Kein Engine-Update-Befehl gesetzt (MC_ENGINE_UPDATE_CMD).")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/swap-update")
|
@router.post("/maintenance/swap-update")
|
||||||
def swap_update(body: SudoReq) -> dict:
|
def swap_update() -> dict:
|
||||||
res = maintenance.swap_update_job(body.sudo_password)
|
res = maintenance.swap_update_job()
|
||||||
if not res:
|
if not res:
|
||||||
raise HTTPException(400, "Kein Router-Update-Befehl gesetzt (MC_SWAP_UPDATE_CMD).")
|
raise HTTPException(400, "Kein Router-Update-Befehl gesetzt (MC_SWAP_UPDATE_CMD).")
|
||||||
return res
|
return res
|
||||||
@@ -65,20 +74,34 @@ def hermes_update() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/update-all")
|
@router.post("/maintenance/update-all")
|
||||||
def update_all(body: SudoReq) -> dict:
|
def update_all() -> dict:
|
||||||
return maintenance.update_all_job(body.sudo_password)
|
return maintenance.update_all_job()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/reboot")
|
@router.post("/maintenance/reboot")
|
||||||
def reboot(body: SudoReq) -> dict:
|
def reboot() -> dict:
|
||||||
return maintenance.reboot(body.sudo_password)
|
return maintenance.reboot()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/maintenance/restart")
|
@router.post("/maintenance/restart")
|
||||||
def restart(body: RestartReq) -> dict:
|
def restart(body: RestartReq) -> dict:
|
||||||
return maintenance.restart_service(body.service, body.sudo_password)
|
return maintenance.restart_service(body.service)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/maintenance/logs")
|
@router.get("/maintenance/logs")
|
||||||
def logs(service: str, lines: int = 200, x_sudo_password: str | None = Header(None)) -> dict:
|
def logs(service: str, lines: int = 200) -> dict:
|
||||||
return maintenance.logs(service, lines, x_sudo_password)
|
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 config import HF_DOWNLOAD_ENV, MODELS_DIR
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from pydantic import BaseModel
|
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
|
from services.fit import evaluate_fit, max_ctx_for
|
||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
router = APIRouter(prefix="/api")
|
||||||
@@ -77,7 +77,9 @@ class InstallReq(BaseModel):
|
|||||||
quant: str = "Q4_K_M"
|
quant: str = "Q4_K_M"
|
||||||
ctx: int | None = None
|
ctx: int | None = None
|
||||||
jinja: bool = False
|
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")
|
@router.get("/hf/search")
|
||||||
@@ -144,8 +146,8 @@ def install(req: InstallReq) -> dict:
|
|||||||
args.append(info["mmproj"])
|
args.append(info["mmproj"])
|
||||||
args += ["--local-dir", str(target)]
|
args += ["--local-dir", str(target)]
|
||||||
env = dict(HF_DOWNLOAD_ENV)
|
env = dict(HF_DOWNLOAD_ENV)
|
||||||
if req.hf_token:
|
if token := geheimnisse.hf_token():
|
||||||
env["HF_TOKEN"] = req.hf_token
|
env["HF_TOKEN"] = token
|
||||||
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
|
job_id = jobengine.start_job(args, f"download {req.repo}", env=env,
|
||||||
on_done=_apply_role if role else None)
|
on_done=_apply_role if role else None)
|
||||||
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
jobengine.attach_download_progress(job_id, str(target), info["total_bytes"])
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api")
|
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):
|
class RunSkillRequest(BaseModel):
|
||||||
skill_name: str
|
skill_name: str
|
||||||
|
|
||||||
@@ -57,8 +71,11 @@ def list_skills():
|
|||||||
|
|
||||||
skills.append({
|
skills.append({
|
||||||
"name": entry.name,
|
"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.",
|
"description": description or "Keine Beschreibung verfügbar.",
|
||||||
"running": entry.name in running_skills
|
"running": _hermes_name(entry.name) in running_skills
|
||||||
})
|
})
|
||||||
return {"skills": skills}
|
return {"skills": skills}
|
||||||
|
|
||||||
@@ -70,13 +87,15 @@ def run_skill(req: RunSkillRequest):
|
|||||||
# Fallback falls lokal (auf Windows) entwickelt wird
|
# Fallback falls lokal (auf Windows) entwickelt wird
|
||||||
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
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
|
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat - mit dem Namen, den Hermes
|
||||||
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
|
# 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:
|
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)
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
with open(log_path, "a") as f:
|
with open(log_path, "a", encoding="utf-8") as f:
|
||||||
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
|
f.write(f"\n--- Starting Skill: {skill} ---\n")
|
||||||
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
||||||
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, MEM0_SERVICE_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
from config import GATEWAY_URL, HERMES_API_URL, LLAMA_SWAP_URL, V1_UPSTREAM, VOICE_SERVICE_URL
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from services import backup as backup_svc
|
from services import backup as backup_svc
|
||||||
@@ -42,13 +42,6 @@ def history(minutes: int = 60) -> dict:
|
|||||||
return metrics_history.history(minutes)
|
return metrics_history.history(minutes)
|
||||||
|
|
||||||
|
|
||||||
def _mem0_reachable() -> bool:
|
|
||||||
try:
|
|
||||||
return httpx.get(f"{MEM0_SERVICE_URL}/health", timeout=2).status_code == 200
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _voice_reachable() -> bool:
|
def _voice_reachable() -> bool:
|
||||||
try:
|
try:
|
||||||
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
return httpx.get(f"{VOICE_SERVICE_URL}/health", timeout=2).status_code == 200
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
|
|||||||
|
|
||||||
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
||||||
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
||||||
geteiltem Mem0 wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
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.
|
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
||||||
|
|
||||||
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||||
@@ -17,7 +17,7 @@ import sys as _sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import httpx
|
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 import APIRouter, File, Form, HTTPException, UploadFile
|
||||||
from fastapi.responses import Response, StreamingResponse
|
from fastapi.responses import Response, StreamingResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
|
|||||||
@router.get("/voice/trace")
|
@router.get("/voice/trace")
|
||||||
def voice_trace(limit: int = 20) -> dict:
|
def voice_trace(limit: int = 20) -> dict:
|
||||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
||||||
Generierung, Mem0 als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
Generierung). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||||
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||||
return {"turns": get_trace(limit)}
|
return {"turns": get_trace(limit)}
|
||||||
|
|
||||||
@@ -289,8 +289,8 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
headers["X-Hermes-Session-Key"] = body.session_key
|
headers["X-Hermes-Session-Key"] = body.session_key
|
||||||
|
|
||||||
async def gen():
|
async def gen():
|
||||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Mem0
|
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung zu EINEM
|
||||||
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
# Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||||
first = True
|
first = True
|
||||||
first_content = 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}
|
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py).
|
||||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||||
try:
|
try:
|
||||||
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
||||||
trace.note_ttfb()
|
trace.note_ttfb()
|
||||||
first = False
|
first = False
|
||||||
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT) —
|
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT) —
|
||||||
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
||||||
if first_content and b'"content"' in chunk:
|
if first_content and b'"content"' in chunk:
|
||||||
trace.note_first_content()
|
trace.note_first_content()
|
||||||
@@ -355,3 +355,71 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
_commit() # Turn immer verbuchen (auch bei Fehler/Abbruch)
|
_commit() # Turn immer verbuchen (auch bei Fehler/Abbruch)
|
||||||
|
|
||||||
return StreamingResponse(gen(), media_type="text/event-stream")
|
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")})
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def list_snapshots() -> dict:
|
|||||||
|
|
||||||
@router.get("/zeitmaschine/inhalt")
|
@router.get("/zeitmaschine/inhalt")
|
||||||
def snapshot_contents(file: str) -> dict:
|
def snapshot_contents(file: str) -> dict:
|
||||||
"""Top-Level-Komponenten eines Snapshots (mem0, hermes, llama-swap, MANIFEST …)."""
|
"""Top-Level-Komponenten eines Snapshots (hermes, llama-swap, MANIFEST …)."""
|
||||||
if not _FILE_RX.match(file):
|
if not _FILE_RX.match(file):
|
||||||
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
raise HTTPException(400, "Ungültiger Snapshot-Name.")
|
||||||
p = backup_svc.BACKUP_DIR / file
|
p = backup_svc.BACKUP_DIR / file
|
||||||
|
|||||||
@@ -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,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Voll-Zustands-Backup (mem0 + Hermes-Configs/Secrets + llama-swap config).
|
Voll-Zustands-Backup (Hermes-Configs/Secrets + llama-swap config).
|
||||||
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
Delegiert an deploy/backup.sh (eine Quelle der Wahrheit, identisch zum systemd-Timer);
|
||||||
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
Restore läuft bewusst nur per CLI (deploy/restore.sh) — siehe docs/BACKUP.md.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Connect: erzeugt saubere, getestete Konfig-Snippets für IDEs/Agenten auf dem
|
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
|
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
|
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.
|
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"
|
return f"http://{host}:{PORT}/v1"
|
||||||
|
|
||||||
|
|
||||||
def build_snippets(host: str = DEFAULT_HOST,
|
def build_snippets(host: str = DEFAULT_HOST) -> dict:
|
||||||
mcp_script_path: str = r"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py",
|
|
||||||
mcp_python: str = "python") -> dict:
|
|
||||||
gw = _gw(host)
|
gw = _gw(host)
|
||||||
mc_url = f"http://{host}:{PORT}"
|
mc_url = f"http://{host}:{PORT}"
|
||||||
|
|
||||||
@@ -88,16 +92,6 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
f'export ANTHROPIC_MODEL="coder"'
|
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 {
|
return {
|
||||||
"host": host,
|
"host": host,
|
||||||
"gateway_url": gw,
|
"gateway_url": gw,
|
||||||
@@ -116,11 +110,8 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
"API-Profile je Modus: Code→coder · Architect/Orchestrator→heavy · "
|
||||||
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
"Ask/Debug→Lane 'chat' (virtuelles Modell, wählt selbst fast oder heavy)."},
|
||||||
"claude_code": {"label": "Claude Code", "lang": "bash", "snippet": claude_code,
|
"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()
|
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 = JOBS[job_id]
|
||||||
job["state"] = "running"
|
job["state"] = "running"
|
||||||
try:
|
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(
|
proc = subprocess.Popen(
|
||||||
actual_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
list(args), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
stdin=subprocess.PIPE if sudo_password is not None else None,
|
stdin=subprocess.DEVNULL,
|
||||||
bufsize=0,
|
bufsize=0,
|
||||||
env={**os.environ, **(env or {})},
|
env={**os.environ, **(env or {})},
|
||||||
)
|
)
|
||||||
_PROCS[job_id] = proc
|
_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)
|
_pump_output(job, proc.stdout)
|
||||||
proc.wait()
|
proc.wait()
|
||||||
job["returncode"] = proc.returncode
|
job["returncode"] = proc.returncode
|
||||||
job["state"] = "canceled" if job.get("canceled") else ("done" if proc.returncode == 0 else "failed")
|
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:
|
except Exception as exc:
|
||||||
_append_log(job, f"[mc] Fehler: {exc}")
|
_append_log(job, f"[mc] Fehler: {exc}")
|
||||||
job["state"] = "failed"
|
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,
|
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]
|
job_id = uuid.uuid4().hex[:12]
|
||||||
# Mask password in log if present in args
|
|
||||||
log_args = list(args)
|
log_args = list(args)
|
||||||
JOBS[job_id] = {
|
JOBS[job_id] = {
|
||||||
"id": job_id, "label": label, "state": "queued", "group": group,
|
"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:
|
if on_done:
|
||||||
JOBS[job_id]["_on_done"] = 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
|
return job_id
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+801
-802
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
||||||
|
|
||||||
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway, Mem0,
|
Prüft periodisch die Kern-Dienste (Engine, Agent-Hirn, Hermes-Gateway,
|
||||||
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
Voice-Sidecar, Platte) und meldet ZUSTANDSWECHSEL in den Melde-Briefkasten
|
||||||
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
(services/announce.py → Lucy spricht es) und via notify.sh (Telegram).
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ import time
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import psutil
|
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
|
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"))
|
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:
|
try:
|
||||||
with httpx.Client(timeout=5.0) as c:
|
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:
|
except Exception:
|
||||||
return False
|
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:
|
def _check_engine() -> bool:
|
||||||
return llamaswap.engine_reachable()
|
return llamaswap.engine_reachable()
|
||||||
|
|
||||||
@@ -70,7 +82,7 @@ CHECKS: dict[str, tuple] = {
|
|||||||
"brain": (_check_brain,
|
"brain": (_check_brain,
|
||||||
"Mein Gehirn lädt nicht — ich kann gerade nicht richtig denken. Ein Neustart der Engine könnte helfen.",
|
"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."),
|
"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 ist ausgefallen — Telegram und meine Tools gehen gerade nicht.",
|
||||||
"Der Agent-Dienst läuft wieder."),
|
"Der Agent-Dienst läuft wieder."),
|
||||||
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import glob
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
from config import MODELS_DIR
|
from config import MODELS_DIR
|
||||||
@@ -197,5 +198,59 @@ def system_status() -> dict:
|
|||||||
"gpu": _gpu_sysfs(),
|
"gpu": _gpu_sysfs(),
|
||||||
"temp": _temps(),
|
"temp": _temps(),
|
||||||
"disk": disk,
|
"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(),
|
"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,14 +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,
|
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
||||||
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
||||||
**Mem0-Retrieve** läuft zwar in Hermes, ruft aber MC2s `/api/memory` per HTTP zurück → messbar und als
|
**Gedächtnis-Abruf** dagegen läuft seit der Ablösung des Sidecars (07.08.2026) Hermes-intern — es
|
||||||
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
|
gibt keinen Rückruf an MC2 mehr, also auch keine Messung; er steckt jetzt in der Hirn-Zeit. Ebenso die
|
||||||
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
|
**Tool-Runden**: im Hermes-LLM-Loop ohne Callback an MC2 → unsichtbar, im „Generierung"-Bucket.
|
||||||
|
|
||||||
STT (davor) und Mem0-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
STT läuft als eigener HTTP-Request VOR dem Turn, ohne Turn-ID. Auf einem EIN-Nutzer-Gerät genügt eine
|
||||||
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer wird global „geparkt" und vom
|
||||||
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
nächsten Chat-Turn eingesammelt (mit Frist-Check). Kein Turn-ID-Durchreichen durch den Lucy-Client
|
||||||
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
nötig.
|
||||||
|
|
||||||
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
||||||
"""
|
"""
|
||||||
@@ -34,11 +34,11 @@ _STAGES: dict[str, deque] = {}
|
|||||||
_TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-Trace)
|
_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.
|
# 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 / Mem0-Retrieve, je
|
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer als
|
||||||
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
# (ms, perf_counter-Zeitstempel). Der nächste Chat-Turn sammelt sie ein und leert sie.
|
||||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None}
|
||||||
|
|
||||||
|
|
||||||
def record_stage(stage: str, ms: float) -> None:
|
def record_stage(stage: str, ms: float) -> None:
|
||||||
@@ -53,8 +53,8 @@ def record_stage(stage: str, ms: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def park(kind: str, ms: float) -> None:
|
def park(kind: str, ms: float) -> None:
|
||||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Mem0-Retrieve als
|
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT läuft davor), global parken,
|
||||||
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
damit der nächste Chat-Turn sie einsammeln kann."""
|
||||||
if ms is None or ms < 0 or kind not in _PARKED:
|
if ms is None or ms < 0 or kind not in _PARKED:
|
||||||
return
|
return
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
@@ -71,17 +71,6 @@ def _take_stt(max_age: float = 20.0) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
|
||||||
"""Geparkten Mem0-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
|
||||||
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:
|
class Timer:
|
||||||
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
||||||
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
||||||
@@ -102,8 +91,7 @@ class TurnTrace:
|
|||||||
"""Ein Per-Turn-Trace für den Voice/Lucy-Chatpfad. In `voice.py` über die Dauer eines Chat-Turns
|
"""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.
|
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
||||||
|
|
||||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen."""
|
||||||
Unter-Detail: mem0 (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
|
||||||
|
|
||||||
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||||
self.id = uuid.uuid4().hex[:8]
|
self.id = uuid.uuid4().hex[:8]
|
||||||
@@ -113,7 +101,7 @@ class TurnTrace:
|
|||||||
self.session_id = (session_id or "")[:24]
|
self.session_id = (session_id or "")[:24]
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
||||||
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Mem0 + TTFT)
|
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Gedächtnis + TTFT)
|
||||||
self.had_images = False
|
self.had_images = False
|
||||||
self.error: str | None = None
|
self.error: str | None = None
|
||||||
|
|
||||||
@@ -130,7 +118,7 @@ class TurnTrace:
|
|||||||
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
||||||
|
|
||||||
def note_first_content(self) -> None:
|
def note_first_content(self) -> None:
|
||||||
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT)."""
|
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT)."""
|
||||||
ms = (time.perf_counter() - self._brain0) * 1000.0
|
ms = (time.perf_counter() - self._brain0) * 1000.0
|
||||||
self.hirn_ms = round(ms, 1)
|
self.hirn_ms = round(ms, 1)
|
||||||
record_stage("chat_first_content", ms)
|
record_stage("chat_first_content", ms)
|
||||||
@@ -138,7 +126,6 @@ class TurnTrace:
|
|||||||
def commit(self) -> dict:
|
def commit(self) -> dict:
|
||||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
||||||
mem0 = _take_retrieve(self.perf0) # rollend bereits in /api/memory erfasst
|
|
||||||
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
||||||
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||||
rec = {
|
rec = {
|
||||||
@@ -151,7 +138,6 @@ class TurnTrace:
|
|||||||
"vision_ms": self.vision_ms,
|
"vision_ms": self.vision_ms,
|
||||||
"hirn_ms": self.hirn_ms,
|
"hirn_ms": self.hirn_ms,
|
||||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||||
"mem0_ms": mem0,
|
|
||||||
"total_ms": round(total, 1),
|
"total_ms": round(total, 1),
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-8
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
||||||
|
|
||||||
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
|
Bisher hingen Re-Warm-Wächter und Health-Wächter (sentry) am
|
||||||
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
||||||
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
||||||
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
||||||
@@ -11,14 +11,16 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
|||||||
In-Process-Nudge aus llamaswap.write_config)
|
In-Process-Nudge aus llamaswap.write_config)
|
||||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||||
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
|
|
||||||
|
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
|
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
||||||
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
||||||
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
||||||
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
||||||
|
|
||||||
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
|
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED)
|
||||||
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
||||||
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +30,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from config import CONFIG_PATH
|
from config import CONFIG_PATH
|
||||||
from services import memory as memory_svc
|
|
||||||
from services import sentry, warmer
|
from services import sentry, warmer
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -66,10 +67,6 @@ async def main() -> None:
|
|||||||
warmer.INTERVAL, CONFIG_WATCH_S)
|
warmer.INTERVAL, CONFIG_WATCH_S)
|
||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
|
||||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
|
||||||
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
|
||||||
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
|
|
||||||
if not tasks:
|
if not tasks:
|
||||||
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ hint=""
|
|||||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||||
ctx="$(cat <<EOF
|
ctx="$(cat <<EOF
|
||||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. 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).
|
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
||||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||||
|
|||||||
@@ -67,6 +67,22 @@ jobs:
|
|||||||
(cd "$d" && npm ci --no-audit --no-fund) || { rot=1; continue; }
|
(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 '"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
|
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"
|
done <<< "$pkg_dateien"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+62
-1
@@ -123,7 +123,7 @@ check_hermes(){
|
|||||||
done
|
done
|
||||||
if [ "$state" = "done" ]; then
|
if [ "$state" = "done" ]; then
|
||||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
||||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft."
|
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Patches, Tools, Voice) grün — alles läuft."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
||||||
@@ -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.)"
|
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 ─────────────────────────────────────────────────────────────────────
|
# ── Lauf ─────────────────────────────────────────────────────────────────────
|
||||||
say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
|
say "Auto-Update-Lauf startet ($(date '+%F %H:%M'))."
|
||||||
pins_init
|
pins_init
|
||||||
@@ -180,3 +237,7 @@ check_hermes
|
|||||||
notify "Commander, die Wochenpflege der Box ist durch — kurz für dich:
|
notify "Commander, die Wochenpflege der Box ist durch — kurz für dich:
|
||||||
$(printf '• %s\n' "${SUMMARY[@]}")"
|
$(printf '• %s\n' "${SUMMARY[@]}")"
|
||||||
say "Fertig."
|
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
|
||||||
|
|||||||
+8
-9
@@ -3,8 +3,10 @@
|
|||||||
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
||||||
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
||||||
#
|
#
|
||||||
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
|
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||||
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
|
# known-good/ (Versions-Manifest, s. u.)
|
||||||
|
# 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.
|
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
||||||
#
|
#
|
||||||
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
||||||
@@ -20,16 +22,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
|
|||||||
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
||||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
TS="$(date +%Y%m%d-%H%M%S)"
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
STAGE="$(mktemp -d)"
|
STAGE="$(mktemp -d)"
|
||||||
trap 'rm -rf "$STAGE"' EXIT
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
mkdir -p "$STAGE/mem0" "$STAGE/hermes" "$STAGE/llama-swap"
|
mkdir -p "$STAGE/hermes" "$STAGE/llama-swap"
|
||||||
|
|
||||||
[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true
|
|
||||||
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
||||||
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
||||||
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
||||||
@@ -60,7 +60,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
|||||||
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||||
|
|
||||||
# pip freeze je venv; mem0-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
# pip freeze je venv; das voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||||
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
"$1" -m pip freeze > "$2" 2>/dev/null \
|
||||||
@@ -68,7 +68,6 @@ freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
|||||||
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
||||||
}
|
}
|
||||||
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
||||||
freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"
|
|
||||||
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -79,7 +78,7 @@ freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
|||||||
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
||||||
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
||||||
echo "python-venvs :"
|
echo "python-venvs :"
|
||||||
for P in "$SRC/backend/.venv/bin/python" "$HOME/.mem0/venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
for P in "$SRC/backend/.venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
||||||
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
||||||
done
|
done
|
||||||
} > "$KG/versions.txt" || true
|
} > "$KG/versions.txt" || true
|
||||||
@@ -92,7 +91,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
|
|||||||
mc2-state backup
|
mc2-state backup
|
||||||
created : $TS
|
created : $TS
|
||||||
host : $(hostname)
|
host : $(hostname)
|
||||||
inhalt : mem0 (chroma + history.db), hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
inhalt : hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
||||||
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
#!/bin/bash
|
||||||
set -e
|
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
|
echo "Starte MC2 Deployment..."
|
||||||
git pull origin main
|
|
||||||
|
# 1. Neuesten Code holen
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
|
||||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||||
echo "Aktualisiere Python Abhängigkeiten..."
|
echo "Aktualisiere Python Abhängigkeiten..."
|
||||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||||
|
|
||||||
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
# 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..."
|
# ‼️ ACHTUNG (27.08.2026): /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit — MC2
|
||||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
# schreibt sie selbst (backend/services/llamaswap.py::write_config), sobald jemand über
|
||||||
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
# die Oberfläche ein Modell hinzufügt oder eine Rolle ändert. deploy/llama-swap.config.yaml
|
||||||
sudo systemctl restart llama-swap
|
# ist nur ein Abzug und hinkt regelmäßig hinterher (erlebt: der Coder-Vollkontext aus 34a9862
|
||||||
fi
|
# 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
|
||||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
# UI Hinzugefügte war nach jedem Deploy weg. Jetzt: erst sichern, dann kopieren, und die
|
||||||
systemctl --user daemon-reload
|
# 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
|
||||||
# 4.5 Hermes Skills synchronisieren
|
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" = "1" ]; then
|
||||||
echo "Synchronisiere Hermes Skills..."
|
echo "Llama-Swap Konfiguration weicht ab — uebersprungen (MC_DEPLOY_SKIP_SWAP_CONFIG=1)."
|
||||||
mkdir -p ~/.hermes/skills
|
else
|
||||||
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
|
SWAP_BAK="/etc/llama-swap/config.yaml.bak-$(date +%Y%m%d-%H%M%S)"
|
||||||
for skill_dir in deploy/skills/*; do
|
echo "Llama-Swap Konfiguration hat sich geändert."
|
||||||
if [ -d "$skill_dir" ]; then
|
echo " Abweichung (live -> Repo-Abzug):"
|
||||||
skill_name=$(basename "$skill_dir" | tr '-' '_')
|
diff -u /etc/llama-swap/config.yaml deploy/llama-swap.config.yaml | head -40 || true
|
||||||
mkdir -p ~/.hermes/skills/"$skill_name"
|
echo " Sichere die laufende Datei nach $SWAP_BAK"
|
||||||
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
|
# 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
|
fi
|
||||||
done
|
fi
|
||||||
|
|
||||||
# 5. Dienste neu starten
|
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||||
echo "Starte Backend neu..."
|
systemctl --user daemon-reload
|
||||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
|
||||||
|
|
||||||
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
-32
@@ -1,35 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
# Post-Update-Check nach einem Hermes-Agent-Update: laeuft unser "Gehirn" noch?
|
||||||
# (Mem0 + die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
# Exit 0 = alles ok, sonst 1 -> der Job wird im UI rot, damit ein kaputtes Gehirn auffaellt.
|
||||||
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
#
|
||||||
|
# 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
|
set -uo pipefail
|
||||||
|
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
||||||
|
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -sf -m 8 "$MC_URL/api/memory" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · /api/memory antwortet"
|
|
||||||
else
|
|
||||||
echo "FAIL · /api/memory antwortet nicht"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
|
||||||
&& 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 ---
|
# --- Hermes-Runtime-Patch-Traeger (20.07.2026): ein Update setzt den Quellbaum ---
|
||||||
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
# --- zurueck → unsere Fixes (needs_input-wartet, Startreife-Gate, Orphan-Guard, ---
|
||||||
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
# --- Kontext-Vererbung, Etappen-Kette) hier RE-eintragen. Exit 10 = etwas neu ---
|
||||||
@@ -46,15 +33,11 @@ if [ -f "$PATCH_APPLY" ]; then
|
|||||||
echo "FAIL · Hermes-Runtime-Patches: ein Patch passt nicht mehr zum aktualisierten Quellcode — pruefen"; fail=1
|
echo "FAIL · Hermes-Runtime-Patches: ein Patch passt nicht mehr zum aktualisierten Quellcode — pruefen"; fail=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "WARN · hermes-patches/apply.py nicht gefunden ($PATCH_APPLY)"
|
# Kein Fehler: Der Patch-Traeger wurde mit dem MC2-Kahlschlag (1e68f62) entfernt. Seine
|
||||||
fi
|
# 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
|
||||||
if ( cd "$HERMES/hermes-agent" && HERMES_HOME="$HERMES" ./venv/bin/python -c \
|
# es sichtbar bleibt, falls jemand wieder Runtime-Patches braucht.
|
||||||
"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'" \
|
echo "WARN · hermes-patches/apply.py nicht vorhanden (mit 1e68f62 bewusst entfernt) — uebersprungen"
|
||||||
>/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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- Config-Drift-Wächter (Lehre aus v0.18, 02.07.2026): Hermes fällt bei unbekannten ---
|
# --- Config-Drift-Wächter (Lehre aus v0.18, 02.07.2026): Hermes fällt bei unbekannten ---
|
||||||
|
|||||||
+61
-41
@@ -1,69 +1,89 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# news-melden.sh — schickt den fertigen Nachrichtenbericht als TEXT und als
|
# news-melden.sh — schickt den fertigen Nachrichtenbericht als TEXT und als
|
||||||
# SPRACHE an den Commander (21.08.2026).
|
# SPRACHNACHRICHT an den Commander (21.08.2026, überarbeitet 22.08.).
|
||||||
#
|
#
|
||||||
# Warum ein Skript statt zwei Anweisungen im Prompt: der Agent soll genau EINEN
|
# Der Agent ruft genau EINEN Befehl auf. Alles danach ist deterministisch —
|
||||||
# Befehl aufrufen. Alles danach ist deterministisch — Zustellung darf nicht davon
|
# Zustellung darf nicht davon abhaengen, ob ein Modell die Reihenfolge einhaelt.
|
||||||
# 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:
|
# Erwartet zwei Dateien, die der Agent vorher geschrieben hat:
|
||||||
# $1 Textfassung (mit Emojis, Links, Formatierung — geht nach Telegram)
|
# $1 Textfassung (mit Emojis, Links, Formatierung — geht nach Telegram)
|
||||||
# $2 Sprechfassung (kurz, ohne Links/Emojis — wird vorgelesen)
|
# $2 Sprechfassung (kurz, ohne Links/Emojis — wird vorgelesen)
|
||||||
#
|
#
|
||||||
# ‼️ STIMME: :8021, nicht :8650.
|
# ‼️ STIMME: :8021, nicht :8650.
|
||||||
# :8021 = lucy-stimme.service — Kyutai pocket-tts 2.1.0, Modell `german_24l`,
|
# :8021 = lucy-stimme.service — Kyutai pocket-tts, Modell `german_24l`,
|
||||||
# Stimme geklont aus ref.mp3. DAS ist Lucy. Laeuft lokal auf der CPU.
|
# Stimme geklont aus ref.mp3. DAS ist Lucy. Lokal, auf der CPU.
|
||||||
# :8650 = MC2s voice-service. Dort sind nur `elevenlabs` und `edge` geladen —
|
# :8650 = MC2s voice-service. Dort sind nur `elevenlabs` und `edge` geladen —
|
||||||
# beides CLOUD, und die Vorgabe ist die ElevenLabs-Stimme
|
# beides CLOUD, und die Vorgabe ist die ElevenLabs-Stimme
|
||||||
# "Artoria/Saber", also HERMES' Stimme, nicht Lucys. Am 21.08. genau
|
# "Artoria/Saber", also HERMES' Stimme, nicht Lucys.
|
||||||
# damit reingefallen: klang deutsch und weiblich, war aber die falsche.
|
|
||||||
# Hermes' eingebautes TTS waere `edge`/`en-US-AriaNeural` — englisch.
|
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
TEXTDATEI="${1:-/tmp/news-text.md}"
|
|
||||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
|
||||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||||
VOICE="${VOICE_URL:-http://127.0.0.1:8021}"
|
VOICE="${VOICE_URL:-http://127.0.0.1:8021}"
|
||||||
HERMES="${HERMES_BIN:-$HOME/.local/bin/hermes}"
|
HERMES="${HERMES_BIN:-$HOME/.local/bin/hermes}"
|
||||||
TON="/tmp/news-stimme.wav"
|
TON="/tmp/news-stimme.wav"
|
||||||
TON_OGG="/tmp/news-stimme.ogg"
|
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
|
if [ ! -s "$TEXTDATEI" ]; then
|
||||||
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------ Text ----
|
# Doppelversand-Sperre. Greift, egal WARUM ein zweiter Aufruf kommt —
|
||||||
# notify.sh erreicht Telegram UND Lucys Briefkasten in einem Aufruf.
|
# 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")"
|
bash "$NOTIFY" -s "" "$(cat "$TEXTDATEI")"
|
||||||
TEXT_OK=$?
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Stimme ----
|
# Stimme abkoppeln: eigene Sitzung, kein Elternprozess, keine offenen Deskriptoren.
|
||||||
# Ab hier ist alles Kuer: klappt die Stimme nicht, ist der Text trotzdem raus.
|
# Damit ueberlebt sie das Ende dieses Skripts und blockiert es nicht.
|
||||||
if [ ! -s "$SPRECHDATEI" ]; then
|
if [ -s "$SPRECHDATEI" ]; then
|
||||||
echo "[news-melden] keine Sprechfassung — nur Text verschickt" >&2
|
setsid nohup bash "$0" --nur-stimme "$SPRECHDATEI" >/dev/null 2>&1 < /dev/null &
|
||||||
exit $TEXT_OK
|
disown 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CODE=$(curl -s -m 180 -X POST "$VOICE/tts" \
|
echo "gemeldet"
|
||||||
-H 'Content-Type: application/json' \
|
exit 0
|
||||||
--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)
|
|
||||||
|
|
||||||
if [ "$CODE" != "200" ] || [ ! -s "$TON" ]; then
|
|
||||||
echo "[news-melden] TTS antwortete mit '$CODE' — Text ist raus, Stimme nicht" >&2
|
|
||||||
exit $TEXT_OK
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Telegram zeigt eine ECHTE Sprachnachricht (runde Blase, Wellenform) nur bei
|
|
||||||
# OGG/Opus. Eine WAV kaeme als Dateianhang an — abspielbar, aber eben kein
|
|
||||||
# Sprachmemo. Deshalb ffmpeg. Mono/48k ist das Format, das Telegram erwartet.
|
|
||||||
if ! ffmpeg -y -loglevel error -i "$TON" -c:a libopus -b:a 32k -ar 48000 -ac 1 "$TON_OGG" 2>/dev/null; then
|
|
||||||
echo "[news-melden] ffmpeg-Wandlung fehlgeschlagen — schicke die WAV als Anhang" >&2
|
|
||||||
TON_OGG="$TON"
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$HERMES" send --to telegram "MEDIA:$TON_OGG" >/dev/null 2>&1 \
|
|
||||||
|| echo "[news-melden] Sprachdatei liess sich nicht senden — Text ist raus" >&2
|
|
||||||
|
|
||||||
exit $TEXT_OK
|
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk
|
|||||||
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
||||||
code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null)
|
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)
|
wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3)
|
||||||
if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen"
|
# 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"
|
elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann"
|
||||||
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
||||||
done
|
done
|
||||||
@@ -158,6 +164,33 @@ echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')"
|
|||||||
echo
|
echo
|
||||||
|
|
||||||
# ------------------------------------------------------------- Warm/Kalt ---
|
# ------------------------------------------------------------- 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"
|
echo "MODELLE IM SPEICHER"
|
||||||
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
||||||
| python3 -c '
|
| python3 -c '
|
||||||
|
|||||||
@@ -42,6 +42,27 @@ if [ -z "$IST" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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)"
|
ROT="$(printf '%s\n' "$IST" | grep -E '^\s+(ROT|ACHTUNG)' || true)"
|
||||||
ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)"
|
ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)"
|
||||||
|
|
||||||
@@ -61,6 +82,9 @@ Regeln:
|
|||||||
Fuellmaterial wird nicht gelesen.
|
Fuellmaterial wird nicht gelesen.
|
||||||
- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG,
|
- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG,
|
||||||
nicht unter Verbesserungen.
|
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.
|
- Wenn alles gruen ist, sag das in einem Satz und hoere auf.
|
||||||
- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter.
|
- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter.
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +4,7 @@ globalTTL: 0
|
|||||||
|
|
||||||
models:
|
models:
|
||||||
Qwen3.6-35B-A3B:
|
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:
|
# 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,
|
# 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
|
# lief aber NIE live — bewusst bei Q8_0 geblieben: Qualität vor ein paar GB, seit der
|
||||||
@@ -25,16 +25,21 @@ models:
|
|||||||
# Qwen 3.8 27B: Dichtes 27B-Modell mit hybrider Linear-Attention (48/64 Schichten linear),
|
# 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.
|
# 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.
|
# 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).
|
||||||
cmd: |
|
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-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 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
|
ttl: 5400
|
||||||
aliases:
|
aliases:
|
||||||
- coder
|
- coder
|
||||||
|
- heavy
|
||||||
capabilities:
|
capabilities:
|
||||||
in: [text, image]
|
in: [text, image]
|
||||||
out: [text]
|
out: [text]
|
||||||
tools: true
|
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:
|
Muse-Glimmer-30B:
|
||||||
# Meta Muse Glimmer 30B: Dichtes 30B-Agenten-Modell mit DFlash-Speculative-Drafting
|
# Meta Muse Glimmer 30B: Dichtes 30B-Agenten-Modell mit DFlash-Speculative-Drafting
|
||||||
# und 1.8B Perception Multimodal Projector. Ideal als Runtime-Debugger & Fehler-Diagnostiker.
|
# und 1.8B Perception Multimodal Projector. Ideal als Runtime-Debugger & Fehler-Diagnostiker.
|
||||||
@@ -60,8 +65,8 @@ models:
|
|||||||
cmd: |
|
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 --no-mmap --jinja --cache-reuse 256 -cram 16384
|
||||||
ttl: 600
|
ttl: 600
|
||||||
aliases:
|
# 04.09.2026: Rolle `heavy` an Qwen3.8-27B abgegeben (AA-Index 52 vs 24, 17 statt 60 GB, 31 t/s mit DFlash2).
|
||||||
- heavy
|
# Ohne Alias = Rollback-Reserve, direkt als `gpt-oss-120b` ansprechbar.
|
||||||
capabilities:
|
capabilities:
|
||||||
in: [text]
|
in: [text]
|
||||||
out: [text]
|
out: [text]
|
||||||
@@ -78,7 +83,7 @@ models:
|
|||||||
out: [text]
|
out: [text]
|
||||||
context: 32768
|
context: 32768
|
||||||
Qwen3-Reranker-0.6B:
|
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!).
|
# 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.
|
# Winzig (~0,7 GB) → in brains (verdrängungssicher); lädt in ~1-2 s, erste Anfrage wärmt.
|
||||||
cmd: |
|
cmd: |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[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
|
Documentation=file:%h/mission-control-v2/docs/BACKUP.md
|
||||||
|
|
||||||
[Service]
|
[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_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||||
Environment=MC_MODELS_DIR=/srv/models
|
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
|
# Gateway-Auszug (UMBAU v3 P1): /v1 roh an den eigenständigen mc2-gateway-Prozess
|
||||||
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
# durchreichen. Diese Zeile entfernen (+ daemon-reload + restart) = Rollback, MC2
|
||||||
# bedient /v1 wieder selbst.
|
# bedient /v1 wieder selbst.
|
||||||
Environment=MC_V1_UPSTREAM=http://127.0.0.1:9010
|
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)
|
# Steward-Auszug (UMBAU v3 P2): Die Wächter-Loops (Re-Warm, Health-Sentry) laufen jetzt im
|
||||||
# laufen jetzt im eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese drei
|
# eigenen Dienst mc2-steward.service — hier deshalb AUS. Diese zwei Zeilen entfernen
|
||||||
# Zeilen entfernen (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
# (+ daemon-reload + restart) = Rollback (Loops wieder im Steuerpult).
|
||||||
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
# Der reminders_loop bleibt bewusst HIER (teilt Datei+CRUD mit /api/reminders).
|
||||||
Environment=MC_REWARM_ENABLED=0
|
Environment=MC_REWARM_ENABLED=0
|
||||||
Environment=MC_SENTRY_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
|
# 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
|
# 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
|
# 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,3 +76,41 @@ Laufzeit**. Laeuft es ab, scheitert der Push des Agenten mit
|
|||||||
wieder. Dauerhafte Loesung waere ein langlebiges Gitea-Token auf die **interne** Adresse
|
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
|
`http://192.168.178.153:3000` (die DDNS-Domain ist nachts durch die Zwangstrennung ohnehin
|
||||||
zeitweise tot).
|
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.
|
||||||
|
|||||||
@@ -60,7 +60,16 @@ if (-not $NurSync) {
|
|||||||
Schritt "Push nach Gitea ($(($ausstehend | Measure-Object).Count) Commit(s))"
|
Schritt "Push nach Gitea ($(($ausstehend | Measure-Object).Count) Commit(s))"
|
||||||
$ausstehend | ForEach-Object { Write-Host " $_" }
|
$ausstehend | ForEach-Object { Write-Host " $_" }
|
||||||
git push origin $zweig
|
git push origin $zweig
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Push fehlgeschlagen. Haengt die DDNS-Domain? Interne Adresse ist http://192.168.178.153:3000" }
|
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"
|
Gut "gepusht"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -15,11 +15,13 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
||||||
DEST_DIR="$MODELS_DIR/mc2-backups"
|
DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||||
SERVICES="mem0-service mission-control-2 hermes-gateway"
|
# 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="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||||
@@ -98,7 +100,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
|
|||||||
|
|
||||||
if [ "$DRY" = 1 ]; then
|
if [ "$DRY" = 1 ]; then
|
||||||
echo "[dry-run] würde zurücksetzen:"
|
echo "[dry-run] würde zurücksetzen:"
|
||||||
[ -d "$STAGE/mem0" ] && echo " • mem0 → $MEM0_DIR (wird ersetzt)"
|
|
||||||
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
||||||
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
||||||
@@ -119,7 +120,8 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
|
|||||||
echo "→ Dienste stoppen…"
|
echo "→ Dienste stoppen…"
|
||||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
systemctl --user stop $SERVICES 2>/dev/null || true
|
||||||
|
|
||||||
if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir -p "$MEM0_DIR"; cp -a "$STAGE/mem0/." "$MEM0_DIR/"; fi
|
# 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/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
||||||
# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed).
|
# Exit 0 = alles ok, sonst 1 → der jobengine-Job wird im UI ROT (state=failed).
|
||||||
#
|
#
|
||||||
# Pendant zu hermes-postcheck.sh (prüft das Gehirn/Mem0); dieser prüft Router+Engine+MC2
|
# Pendant zu hermes-postcheck.sh (prüft das Gehirn); dieser prüft Router+Engine+MC2
|
||||||
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
|
# inkl. einer ECHTEN 1-Token-Inferenz (beweist, dass ein Modell wirklich lädt & generiert).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
SWAP_URL="${MC_LLAMA_SWAP_URL:-http://127.0.0.1:8080}"
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
|
||||||
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
|
BRAIN="${MC_WARMUP_MODELS:-fast}"; BRAIN="${BRAIN%% *}" # erstes Modell, falls Liste
|
||||||
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Mem0/Gedächtnis)
|
EMBED="${MC_WARMUP_EMBED:-embed}"; EMBED="${EMBED%% *}" # Embedding-Modell (Reranker-Stufe)
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||||
@@ -48,14 +47,14 @@ else
|
|||||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3b. Embedding-Modell (für Mem0/Gedächtnis) lädt und liefert einen Vektor?
|
# 3b. Embedding-Modell lädt und liefert einen Vektor?
|
||||||
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||||
else
|
else
|
||||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Mem0/Gedächtnis betroffen)"; fail=1
|
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
||||||
@@ -85,12 +84,8 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
|
# (Schritt 5 „Gedächtnis-Sidecar erreichbar" entfiel am 27.08.2026: der Dienst auf :8765 ist
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
# seit der Ablösung im August tot, der Check konnte nur noch rot werden. docs/wissen/VERDIKTE.md)
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$fail" -eq 0 ]; then
|
if [ "$fail" -eq 0 ]; then
|
||||||
echo "=== STACK OK ✓ ==="
|
echo "=== STACK OK ✓ ==="
|
||||||
|
|||||||
@@ -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": [
|
"eintraege": [
|
||||||
{
|
{
|
||||||
"key": "mmq-prefill-gfx1151",
|
"key": "qwen-modelle",
|
||||||
"typ": "github_issue",
|
"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",
|
"repo": "ggml-org/llama.cpp",
|
||||||
"nummer": 21284,
|
"nummer": 28243,
|
||||||
"bedingung": "Issue geschlossen = MMQ-Tuning für gfx1151 vermutlich gemerged",
|
"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)."
|
||||||
"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)."
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"key": "rocm-releases",
|
||||||
"typ": "github_release",
|
"typ": "github_release",
|
||||||
"repo": "ROCm/ROCm",
|
"repo": "ROCm/ROCm",
|
||||||
"bedingung": "Neuer ROCm-Release seit letzter Meldung",
|
"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)."
|
||||||
"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."
|
},
|
||||||
|
{
|
||||||
|
"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",
|
"key": "electron-major",
|
||||||
"typ": "github_release_major",
|
"typ": "github_release_major",
|
||||||
"repo": "electron/electron",
|
"repo": "electron/electron",
|
||||||
"bedingung": "Neue MAJOR-Version seit letzter Meldung",
|
"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."
|
||||||
"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."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "pocket-tts",
|
"key": "pocket-tts",
|
||||||
"typ": "pypi",
|
"typ": "pypi",
|
||||||
"paket": "pocket-tts",
|
"paket": "pocket-tts",
|
||||||
"bedingung": "Neue Version auf PyPI",
|
"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.)."
|
||||||
"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.)."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "lmstudio-bionic",
|
"key": "lmstudio-bionic",
|
||||||
"typ": "github_issue",
|
"typ": "github_issue",
|
||||||
"repo": "lmstudio-ai/lmstudio-bug-tracker",
|
"repo": "lmstudio-ai/lmstudio-bug-tracker",
|
||||||
"nummer": 2185,
|
"nummer": 2185,
|
||||||
"bedingung": "Issue geschlossen = Bionic-Linux-Port vermutlich da",
|
"warum": "LM Studio Bionic: erst interessant bei Linux-Port (dieses Issue), Custom-Endpoint-Support oder CLI. Bis dahin bleibt die Box :9001 draussen."
|
||||||
"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."
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Mem0, Voice) altern
|
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
|
||||||
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
||||||
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
||||||
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
|
VENVS=("$HOME/.voice/venv:voice")
|
||||||
AUDIT_VENV="$HOME/.venv-audit"
|
AUDIT_VENV="$HOME/.venv-audit"
|
||||||
HERMES="$HOME/.local/bin/hermes"
|
HERMES="$HOME/.local/bin/hermes"
|
||||||
MONAT="$(date '+%Y-%m')"
|
MONAT="$(date '+%Y-%m')"
|
||||||
|
|||||||
@@ -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.
|
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
||||||
#
|
#
|
||||||
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
||||||
# (Qwen3-Embedding-0.6B, für Mem0/Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
# (Qwen3-Embedding-0.6B). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||||
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
||||||
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||||
# anstoßen, sonst bleibt es kalt.
|
# anstoßen, sonst bleibt es kalt.
|
||||||
|
|||||||
+325
-328
@@ -1,328 +1,325 @@
|
|||||||
# Konzept: Bare-Metal-Wiederaufbau + First-Run-Wizard (VOLLSTÄNDIG)
|
# Konzept: Bare-Metal-Wiederaufbau + First-Run-Wizard (VOLLSTÄNDIG)
|
||||||
|
|
||||||
> **Zweck:** Plan für den „hard crash"-Fall — die Box (`tobisniceaiarbeitstier`, Ubuntu 26.04,
|
> **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.
|
> 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,
|
> **Anspruch:** *ALLES* ist erfasst — Engine, Hermes-Konfig 1:1, Voice/Klonstimme,
|
||||||
> Memory, Browser, MCP, Skills, Secrets. Pro Komponente entscheidet der Nutzer im Wizard:
|
> Memory, Browser, MCP, Skills, Secrets. Pro Komponente entscheidet der Nutzer im Wizard:
|
||||||
> **1:1 zurück** · **neu/Default** · **weglassen**.
|
> **1:1 zurück** · **neu/Default** · **weglassen**.
|
||||||
>
|
>
|
||||||
> Dieses Dokument ist die **Spezifikation für eine künftige Implementierungs-Session** — es baut
|
> Dieses Dokument ist die **Spezifikation für eine künftige Implementierungs-Session** — es baut
|
||||||
> noch nichts. Stand: 2026-06-28.
|
> noch nichts. Stand: 2026-06-28.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Leitprinzip: 1:1 ODER modular — der Nutzer wählt
|
## 1. Leitprinzip: 1:1 ODER modular — der Nutzer wählt
|
||||||
|
|
||||||
Der Wizard behandelt jede Komponente als **eigene Kachel mit drei Modi**:
|
Der Wizard behandelt jede Komponente als **eigene Kachel mit drei Modi**:
|
||||||
|
|
||||||
| Modus | Bedeutung |
|
| Modus | Bedeutung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 📦 **1:1 aus Backup** | Exakter alter Zustand wird zurückgespielt (Configs, Daten, Klonstimme, Avatare). |
|
| 📦 **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, Default-Avatar). |
|
| 🆕 **Neu / Default** | Frische Installation mit sinnvollen Defaults (z.B. entfesseltes Hermes-Profil). |
|
||||||
| ⏭️ **Weglassen** | Komponente wird (vorerst) nicht installiert. |
|
| ⏭️ **Weglassen** | Komponente wird (vorerst) nicht installiert. |
|
||||||
|
|
||||||
Ein „Alles 1:1"-Knopf wählt überall 📦 (wo ein Backup existiert), sonst 🆕. So bekommt der Nutzer
|
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.
|
entweder den exakten alten Stand oder kann gezielt entrümpeln.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Was es schon gibt (wiederverwenden)
|
## 2. Was es schon gibt (wiederverwenden)
|
||||||
|
|
||||||
| Asset | Datei | Deckt ab |
|
| Asset | Datei | Deckt ab |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Code-Deploy | `deploy/deploy.sh` | git pull + venvs + Units + Restart. **Setzt Engine/llama-swap/Dirs/venvs voraus.** |
|
| 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). |
|
| 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). |
|
| Restore | `deploy/restore.sh` | Spielt Tarball zurück (mit Pre-Restore-Sicherung). |
|
||||||
| Engine | `deploy/provision-engine.sh` | llama.cpp Vulkan/RADV-Build (root). |
|
| 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). |
|
| 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. |
|
| Updates | `deploy/update-engine.sh`, `update-swap.sh` | Laufende Engine/Router-Updates. |
|
||||||
| Postchecks | `deploy/stack-postcheck.sh`, `hermes-postcheck.sh` | Funktionsprüfung → Wizard-Verifikationsschritt. |
|
| Postchecks | `deploy/stack-postcheck.sh`, `hermes-postcheck.sh` | Funktionsprüfung → Wizard-Verifikationsschritt. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. VOLLSTÄNDIGER Komponenten-Katalog
|
## 3. VOLLSTÄNDIGER Komponenten-Katalog
|
||||||
|
|
||||||
> **Scan-verifiziert (2026-06-28)** gegen `backend/config.py`, alle `backend/services/*` & `routers/*`,
|
> **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/*`.
|
> `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.
|
> Alle persistenten Schreibpfade, Dienste, Secrets und Env-Vars sind unten erfasst.
|
||||||
|
|
||||||
Legende Restore-Quelle: 📦 = aus Backup-Tarball · ⬇️ = Re-Download/Install (Skript) · 🌐 = Git ·
|
Legende Restore-Quelle: 📦 = aus Backup-Tarball · ⬇️ = Re-Download/Install (Skript) · 🌐 = Git ·
|
||||||
🔑 = Secret (Nutzer/Generieren) · 🆕 = im Wizard neu gewählt.
|
🔑 = Secret (Nutzer/Generieren) · 🆕 = im Wizard neu gewählt.
|
||||||
„Im Backup?" = deckt der **aktuelle** `backup.sh` es ab.
|
„Im Backup?" = deckt der **aktuelle** `backup.sh` es ab.
|
||||||
|
|
||||||
### A · OS & System (L0, root/sudo, einmalig)
|
### A · OS & System (L0, root/sudo, einmalig)
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Ubuntu 26.04, User `hitonabi`, `enable-linger` | — | ⬇️ manuell | n/a |
|
| Ubuntu 26.04, User `hitonabi`, `enable-linger` | — | ⬇️ manuell | n/a |
|
||||||
| System-Pakete: python3.14+venv, git, curl, jq, ttyd, uv | — | ⬇️ apt/curl | nein |
|
| System-Pakete: python3.14+venv, git, curl, jq, ttyd, uv | — | ⬇️ apt/curl | nein |
|
||||||
| Vulkan-Stack: mesa-vulkan-drivers, libvulkan1, vulkan-tools | — | ⬇️ apt | nein |
|
| Vulkan-Stack: mesa-vulkan-drivers, libvulkan1, vulkan-tools | — | ⬇️ apt | nein |
|
||||||
| Chrome-Libs (Browser): `agent-browser install --with-deps` | — | ⬇️ 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 |
|
| Verzeichnis-Layout: `/srv/models/{,mem0,mc2-backups,drafts}`, `/etc/llama-swap/` | — | ⬇️ mkdir+chown | nein |
|
||||||
|
|
||||||
### B · Inferenz-Engine + Router (L1, root)
|
### B · Inferenz-Engine + Router (L1, root)
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| 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.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** 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 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** |
|
| llama-swap-Config (Modelle/Rollen) | `/etc/llama-swap/config.yaml` | 📦 | **ja** |
|
||||||
| Draft-Modelle (Spec-Decoding) | `/srv/models/drafts/` | ⬇️ Re-Download | nein |
|
| Draft-Modelle (Spec-Decoding) | `/srv/models/drafts/` | ⬇️ Re-Download | nein |
|
||||||
|
|
||||||
### C · MC2-App (L2, userspace)
|
### C · MC2-App (L2, userspace)
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| MC2-Code | `~/mission-control-v2` | 🌐 Git (Gitea) | n/a |
|
| MC2-Code | `~/mission-control-v2` | 🌐 Git (Gitea) | n/a |
|
||||||
| Backend-venv (Python 3.14) | `backend/.venv` | ⬇️ deploy.sh | nein (rebuild) |
|
| 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 |
|
| Frontend (gebaut) | `frontend/dist` | 🌐 Git | n/a |
|
||||||
| systemd-User-Dienst `mission-control-2` (`:9001`) | `~/.config/systemd/user/` | ⬇️ deploy.sh | nein |
|
| systemd-User-Dienst `mission-control-2` (`:9001`) | `~/.config/systemd/user/` | ⬇️ deploy.sh | nein |
|
||||||
|
|
||||||
### D · 3D-Avatar (Sprechen-Tab)
|
### D · 3D-Avatar — **ausgebaut (28.08.2026)**
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
|
||||||
|---|---|---|---|
|
MC2 hat keinen Avatar mehr. `frontend/public/avatar.vrm` (24,5 MB) lag im Auslieferungsordner,
|
||||||
| Default-Avatar (VRM) | `frontend/public/avatar.vrm` (→ dist) | 🌐 Git | n/a |
|
wurde aber von keiner Zeile des Frontends referenziert — der Renderer `Avatar3D.tsx` war schon
|
||||||
| Renderer | `frontend/src/components/voice/Avatar3D.tsx` | 🌐 Git | n/a |
|
vorher verschwunden. Beides ist beim v3-Umbau (Etappe P0) entfernt worden; damit fällt auch die
|
||||||
| **Eigene/zusätzliche Avatare** (falls Nutzer welche ablegt) | **TODO: Ablageort definieren** (z.B. `/srv/models/avatars/` + DB-Verweis) | 📦/🆕 | **nein (Lücke)** |
|
`.gitignore`-Sonderregel und der Direkt-Deploy-Schritt weg. **Nichts wiederherzustellen.**
|
||||||
|
|
||||||
> Heute ist der Avatar **fest** (`avatar.vrm`, kommt mit dem Git-Frontend zurück). Wenn künftig
|
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
||||||
> Nutzer-Avatare hochgeladen werden, brauchen sie einen persistenten Ablageort, der ins Backup geht.
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
| Voice-venv (Python 3.12) | `~/.voice/venv` | ⬇️ install.sh | nein (rebuild) |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| STT (faster-whisper Modell) | `~/.voice/` (Cache) | ⬇️ install.sh / 1. Start | nein |
|
||||||
|---|---|---|---|
|
| Piper-Binary + dt. Stimmen (thorsten/kerstin) | `~/.voice/piper`, `~/.voice/voices` | ⬇️ install.sh | nein |
|
||||||
| Voice-venv (Python 3.12) | `~/.voice/venv` | ⬇️ install.sh | nein (rebuild) |
|
| Chatterbox (Premium-TTS, CPU-torch) | venv | ⬇️ install.sh (best-effort) | nein |
|
||||||
| STT (faster-whisper Modell) | `~/.voice/` (Cache) | ⬇️ install.sh / 1. Start | nein |
|
| **Klonstimme / Voice-Referenz-Audio** (Nutzer) | `~/.voice/refs/ref.wav` (`VOICE_REFS_DIR`, `/api/voice/reference`) | 📦 | **nein → Anhang B** |
|
||||||
| Piper-Binary + dt. Stimmen (thorsten/kerstin) | `~/.voice/piper`, `~/.voice/voices` | ⬇️ install.sh | nein |
|
| ElevenLabs-Key | `~/.hermes/.env` | 🔑 | ja |
|
||||||
| Chatterbox (Premium-TTS, CPU-torch) | venv | ⬇️ install.sh (best-effort) | nein |
|
| Stimm-/Lautstärke-Wahl | Browser-`localStorage` (pro Gerät) | 🆕 client | n/a |
|
||||||
| **Klonstimme / Voice-Referenz-Audio** (Nutzer) | `~/.voice/refs/ref.wav` (`VOICE_REFS_DIR`, `/api/voice/reference`) | 📦 | **nein → Anhang B** |
|
| Dienst `voice-service` (`:8650`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||||
| ElevenLabs-Key | `~/.hermes/.env` | 🔑 | ja |
|
|
||||||
| Stimm-/Lautstärke-Wahl | Browser-`localStorage` (pro Gerät) | 🆕 client | n/a |
|
### F · Mem0 (Langzeitgedächtnis)
|
||||||
| Dienst `voice-service` (`:8650`) | systemd-User | ⬇️ deploy.sh | nein |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### F · Mem0 (Langzeitgedächtnis)
|
| Mem0-venv (Python 3.12, uv) | `~/.mem0/venv` | ⬇️ deploy.sh | nein (rebuild) |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| **Gedächtnis-Daten (Chroma + history.db)** | `/srv/models/mem0/` | 📦 | **ja** |
|
||||||
|---|---|---|---|
|
| Hermes-Memory-Plugin | `~/.hermes/plugins/mc2-memory/` | 📦/🌐 | ja |
|
||||||
| Mem0-venv (Python 3.12, uv) | `~/.mem0/venv` | ⬇️ deploy.sh | nein (rebuild) |
|
| Dienst `mem0-service` (`:8765`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||||
| **Gedächtnis-Daten (Chroma + history.db)** | `/srv/models/mem0/` | 📦 | **ja** |
|
|
||||||
| Hermes-Memory-Plugin | `~/.hermes/plugins/mc2-memory/` | 📦/🌐 | ja |
|
### G · Hermes-Agent (das Herz)
|
||||||
| Dienst `mem0-service` (`:8765`) | systemd-User | ⬇️ deploy.sh | nein |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### G · Hermes-Agent (das Herz)
|
| Hermes-Code | `~/.hermes/hermes-agent` | 🌐 Git (NousResearch) | nein (re-clone) |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Bundled Node v22 | `~/.hermes/node/bin` | ⬇️ (kommt mit Hermes) | nein |
|
||||||
|---|---|---|---|
|
| Hermes-venv | `~/.hermes/hermes-agent/venv` | ⬇️ rebuild | nein |
|
||||||
| Hermes-Code | `~/.hermes/hermes-agent` | 🌐 Git (NousResearch) | nein (re-clone) |
|
| **`config.yaml` 1:1** (Toolsets, MCP, Engine, Browser, Personalities, alles) | `~/.hermes/config.yaml` | 📦 | **ja** |
|
||||||
| Bundled Node v22 | `~/.hermes/node/bin` | ⬇️ (kommt mit Hermes) | nein |
|
| **`.env` (Secrets: TELEGRAM_BOT_TOKEN, API_SERVER_KEY, ElevenLabs …)** | `~/.hermes/.env` | 📦/🔑 | **ja** |
|
||||||
| Hermes-venv | `~/.hermes/hermes-agent/venv` | ⬇️ rebuild | nein |
|
| Plugins | `~/.hermes/plugins/` | 📦 | ja |
|
||||||
| **`config.yaml` 1:1** (Toolsets, MCP, Engine, Browser, Personalities, alles) | `~/.hermes/config.yaml` | 📦 | **ja** |
|
| **Skills (eigene)** | `~/.hermes/skills/` (45M; `.hub`-Cache re-downloadbar) | 📦 | **nein → Anhang B (ohne .hub)** |
|
||||||
| **`.env` (Secrets: TELEGRAM_BOT_TOKEN, API_SERVER_KEY, ElevenLabs …)** | `~/.hermes/.env` | 📦/🔑 | **ja** |
|
| Sessions/History (optional) | `~/.hermes/sessions/` (856K) | 📦 | **nein → Anhang B** |
|
||||||
| Plugins | `~/.hermes/plugins/` | 📦 | ja |
|
| Checkpoints (optional) | `~/.hermes/checkpoints/` (existiert nicht) | ⏭️ skip | nein |
|
||||||
| **Skills (eigene)** | `~/.hermes/skills/` (45M; `.hub`-Cache re-downloadbar) | 📦 | **nein → Anhang B (ohne .hub)** |
|
| **Token-/Ersparnis-Statistik** | `~/.hermes/token_stats.json` | 📦 | **nein → Anhang B** |
|
||||||
| Sessions/History (optional) | `~/.hermes/sessions/` (856K) | 📦 | **nein → Anhang B** |
|
| Dienst `hermes-gateway` (`:8642`) | systemd-User | ⬇️ | nein |
|
||||||
| Checkpoints (optional) | `~/.hermes/checkpoints/` (existiert nicht) | ⏭️ skip | nein |
|
| Desktop-Gateway `hermes-builtin-ui` (`:9119`) | systemd-User | ⬇️ deploy.sh | nein |
|
||||||
| **Token-/Ersparnis-Statistik** | `~/.hermes/token_stats.json` | 📦 | **nein → Anhang B** |
|
| 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** |
|
||||||
| Dienst `hermes-gateway` (`:8642`) | systemd-User | ⬇️ | nein |
|
| Desktop-Gateway-Token-Kopie | `~/.hermes/desktop-gateway-token` (chmod 600) | 🔑 | **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** |
|
### H · MCP-Server (4)
|
||||||
| Desktop-Gateway-Token-Kopie | `~/.hermes/desktop-gateway-token` (chmod 600) | 🔑 | **nein** |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### H · MCP-Server (4)
|
| `mission-control-memory`, `-stack` (MC2-venv) | `~/mission-control-v2/mcp/*.py` | 🌐 Git | über config.yaml |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| `hermes-pc-control`, `hermes-web-fetch` (Hermes-venv) | dito | 🌐 Git | über config.yaml |
|
||||||
|---|---|---|---|
|
| MCP-Verdrahtung | `~/.hermes/config.yaml → mcp_servers` | 📦 | ja |
|
||||||
| `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 |
|
### I · Browser-Stack
|
||||||
| MCP-Verdrahtung | `~/.hermes/config.yaml → mcp_servers` | 📦 | ja |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### I · Browser-Stack
|
| agent-browser (npm global) | `~/.local/...`, symlink `~/.hermes/node/bin` | ⬇️ npm i -g | nein |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Chrome (engine) + System-Libs | `~/.agent-browser/browsers/` + apt-Libs | ⬇️ install --with-deps | nein |
|
||||||
|---|---|---|---|
|
| lightpanda (optional, leicht) | `~/.local/bin/lightpanda` | ⬇️ Download | nein |
|
||||||
| agent-browser (npm global) | `~/.local/...`, symlink `~/.hermes/node/bin` | ⬇️ npm i -g | nein |
|
| Browser-Verdrahtung (engine=chrome, toolset) | `~/.hermes/config.yaml` | 📦 | ja |
|
||||||
| Chrome (engine) + System-Libs | `~/.agent-browser/browsers/` + apt-Libs | ⬇️ install --with-deps | nein |
|
|
||||||
| lightpanda (optional, leicht) | `~/.local/bin/lightpanda` | ⬇️ Download | nein |
|
### J · Modelle (GGUF — der große Brocken)
|
||||||
| Browser-Verdrahtung (engine=chrome, toolset) | `~/.hermes/config.yaml` | 📦 | ja |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### J · Modelle (GGUF — der große Brocken)
|
| GGUF-Modelle (Brain, fast, heavy, coder, vision, embedding …) | `/srv/models/` | ⬇️ Re-Download aus llama-swap-Manifest | **nein (zu groß, bewusst)** |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
|
||||||
|---|---|---|---|
|
### K · Extern (anderer Rechner)
|
||||||
| GGUF-Modelle (Brain, fast, heavy, coder, vision, embedding …) | `/srv/models/` | ⬇️ Re-Download aus llama-swap-Manifest | **nein (zu groß, bewusst)** |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
|
|---|---|---|---|
|
||||||
### K · Extern (anderer Rechner)
|
| PC-Executor (Windows) | `client/hermes-pc/` → Scheduled Task | 🌐 Git + Task | n/a (anderer Host) |
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Hermes Desktop (Windows-App) | `%LOCALAPPDATA%\hermes` + `connection.json` in `%APPDATA%\Hermes\` | ⬇️ Hermes-Setup.exe + Remote-URL/Token neu eintragen | n/a (anderer Host) |
|
||||||
|---|---|---|---|
|
|
||||||
| 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
|
||||||
## 4. Ziel-Architektur
|
- **Idempotent & resumierbar** (Schritt-Marker in `~/.mc2-bootstrap.state`).
|
||||||
|
- **Getrennt nach sudo-Bedarf**: `bootstrap-root.sh` (L0+L1, bewusst mit sudo) + `bootstrap.sh`
|
||||||
### 4.1 `deploy/bootstrap.sh` — orchestrierter From-Zero-Lauf
|
(L2+, sudo-frei = Kern ist `deploy.sh`). Passt zum Nordstern „Runtime ohne sudo".
|
||||||
- **Idempotent & resumierbar** (Schritt-Marker in `~/.mc2-bootstrap.state`).
|
- **Mündet in den Wizard**: startet MC2 im First-Run-Modus, gibt Wizard-URL aus.
|
||||||
- **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".
|
Phasen: `0 Vorflug → 1[sudo] System+Dirs → 2[sudo] Engine+llama-swap → 3 Code(MC2+Hermes+Node)
|
||||||
- **Mündet in den Wizard**: startet MC2 im First-Run-Modus, gibt Wizard-URL aus.
|
→ 4 venvs(backend/mem0/voice/hermes) → 5 Browser → 6 deploy.sh(Units/enable/restart)
|
||||||
|
→ 7 Restore(optional) → 8 Wizard hoch + postcheck`.
|
||||||
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)
|
### 4.2 First-Run-Wizard (browserbasiert, von MC2 serviert)
|
||||||
→ 7 Restore(optional) → 8 Wizard hoch + postcheck`.
|
MC2 erkennt unkonfigurierten Zustand → Frontend-Route `/setup` statt Dashboard. Schritte:
|
||||||
|
|
||||||
### 4.2 First-Run-Wizard (browserbasiert, von MC2 serviert)
|
1. **Systemcheck** — Live-Ampel je Komponente aus §3 (`GET /api/setup/status` + `…/system/services`).
|
||||||
MC2 erkennt unkonfigurierten Zustand → Frontend-Route `/setup` statt Dashboard. Schritte:
|
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
|
||||||
1. **Systemcheck** — Live-Ampel je Komponente aus §3 (`GET /api/setup/status` + `…/system/services`).
|
📦/🆕/⏭️ (siehe §1). Zeigt Größe + ob Backup-Daten vorhanden.
|
||||||
2. **Restore-Quelle wählen** — Backup-Tarball erkennen → globaler Modus „Alles 1:1" / „selektiv" / „frisch".
|
4. **Netzwerk & Identität** — Box-IP (→ `HERMES_TERMINAL_URL`, `PC_EXECUTOR_URL`), Hostname.
|
||||||
3. **Komponenten-Auswahl (Kernstück)** — pro Katalog-Eintrag aus §3 eine Kachel mit
|
5. **Secrets** 🔑 — `TELEGRAM_BOT_TOKEN`, erlaubte User-ID, `API_SERVER_KEY` (oder generieren),
|
||||||
📦/🆕/⏭️ (siehe §1). Zeigt Größe + ob Backup-Daten vorhanden.
|
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
||||||
4. **Netzwerk & Identität** — Box-IP (→ `HERMES_TERMINAL_URL`, `PC_EXECUTOR_URL`), Hostname.
|
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
||||||
5. **Secrets** 🔑 — `TELEGRAM_BOT_TOKEN`, erlaubte User-ID, `API_SERVER_KEY` (oder generieren),
|
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
||||||
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
7. **Voice** — Stimme/Klonstimme
|
||||||
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
||||||
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
||||||
7. **Avatar & Voice** — Avatar wählen (Default-VRM oder eigener), Stimme/Klonstimme
|
Fortschritt `GET /api/jobs`) ODER geführte Discover-Neuauswahl. Reihenfolge: Brain → heavy → Rest.
|
||||||
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
9. **Externe Checkliste** — PC-Executor (Windows-Task), Hermes Desktop (App + Token) als Haken.
|
||||||
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
10. **Verifikation & Abschluss** — `stack-postcheck.sh` + `hermes-postcheck.sh` → grün/rot-Liste → Dashboard.
|
||||||
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.
|
**Technik:** neuer Router `backend/routers/setup.py`
|
||||||
10. **Verifikation & Abschluss** — `stack-postcheck.sh` + `hermes-postcheck.sh` → grün/rot-Liste → Dashboard.
|
(`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`.
|
||||||
**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
|
||||||
## 5. Backup-Scope für echtes 1:1 ERWEITERN (umzusetzen — Snippet in Anhang B)
|
liegt das **fertige Erweiterungs-Snippet in Anhang B** — es ergänzt den Tarball um:
|
||||||
|
|
||||||
Der **aktuelle** `backup.sh` (live auf der Box, unverändert) reicht für „1:1" nicht. Für die Bau-Session
|
- **Voice-Klonstimme** → `~/.voice/refs/` (`ref.wav`)
|
||||||
liegt das **fertige Erweiterungs-Snippet in Anhang B** — es ergänzt den Tarball um:
|
- **Token-/Ersparnis-Statistik** → `~/.hermes/token_stats.json`
|
||||||
|
- **Hermes-Skills** → `~/.hermes/skills/` (re-downloadbarer `.hub`-Cache per `tar --exclude` ausgelassen)
|
||||||
- **Voice-Klonstimme** → `~/.voice/refs/` (`ref.wav`)
|
- **Hermes-Sessions/History** → `~/.hermes/sessions/`
|
||||||
- **Token-/Ersparnis-Statistik** → `~/.hermes/token_stats.json`
|
|
||||||
- **Hermes-Skills** → `~/.hermes/skills/` (re-downloadbarer `.hub`-Cache per `tar --exclude` ausgelassen)
|
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
||||||
- **Hermes-Sessions/History** → `~/.hermes/sessions/`
|
mit neu starten.
|
||||||
|
|
||||||
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
**Offen bleibt:** nichts — der letzte offene Punkt (eigene Avatare) ist mit dem Avatar-Ausbau erledigt (§3·D).
|
||||||
mit neu starten.
|
|
||||||
|
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
||||||
**Offen bleibt:** eigene Avatare (sobald Upload existiert — Ablageort heute undefiniert, siehe §3·D).
|
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
||||||
|
|
||||||
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
→ Aufgabe der Bau-Session: `backup.sh` + `restore.sh` um diese Pfade erweitern (mit klarer
|
||||||
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
„opt-in für große/optionale Teile"-Logik), und den MANIFEST-Inhalt entsprechend.
|
||||||
|
|
||||||
→ 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.
|
||||||
## 6. Secrets-Strategie
|
- Nie ins Git, nie in Logs, nie in die MC2-DB. **Off-Box-Spiegelung des Tarballs noch offen** ([[mc2-backup-restore]]).
|
||||||
- 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.
|
## 7. Modell-Strategie
|
||||||
- Nie ins Git, nie in Logs, nie in die MC2-DB. **Off-Box-Spiegelung des Tarballs noch offen** ([[mc2-backup-restore]]).
|
- **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`.
|
||||||
## 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
|
||||||
## 8. Implementierungs-Reihenfolge (neue Session)
|
als fertiges Snippet in **Anhang B**. **Keine offenen Box-Fragen mehr** außer §10·3–6. → mit Schritt 2 starten.
|
||||||
1. ✅ **Box-Verifikation erledigt (2026-06-28, nur gelesen — nichts verändert):** llama-swap.service-Inhalt
|
2. `backup.sh`/`restore.sh` um die 1:1-Lücken erweitern (§5).
|
||||||
in **Anhang A**; skills/sessions/refs/token_stats existieren (Pfade in §3 verifiziert); Backup-Erweiterung
|
3. `deploy/bootstrap.sh` + `bootstrap-root.sh` (Phasen, State-Marker); llama-swap-Install skripten.
|
||||||
als fertiges Snippet in **Anhang B**. **Keine offenen Box-Fragen mehr** außer §10·3–6. → mit Schritt 2 starten.
|
4. `backend/routers/setup.py` + First-Run-Gate.
|
||||||
2. `backup.sh`/`restore.sh` um die 1:1-Lücken erweitern (§5).
|
5. Frontend `/setup`-Wizard (Schritte 1–10), Komponenten-Auswahl als Kernstück; bestehende Views
|
||||||
3. `deploy/bootstrap.sh` + `bootstrap-root.sh` (Phasen, State-Marker); llama-swap-Install skripten.
|
(Cockpit/AgentView/Sprechen) wiederverwenden.
|
||||||
4. `backend/routers/setup.py` + First-Run-Gate.
|
6. Modell-Restore-aus-Manifest.
|
||||||
5. Frontend `/setup`-Wizard (Schritte 1–10), Komponenten-Auswahl als Kernstück; bestehende Views
|
7. **Abnahme in frischer VM** (§9).
|
||||||
(Cockpit/AgentView/Sprechen) wiederverwenden.
|
|
||||||
6. Modell-Restore-aus-Manifest.
|
Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
||||||
7. **Abnahme in frischer VM** (§9).
|
|
||||||
|
## 9. Abnahmekriterien
|
||||||
Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
||||||
|
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), Browser funktionieren.
|
||||||
## 9. Abnahmekriterien
|
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
||||||
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
||||||
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), 3D-Avatar, Browser funktionieren.
|
- Idempotenz: zweiter Lauf = No-op.
|
||||||
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
|
||||||
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
## 10. Offene Entscheidungen (vor dem Bau)
|
||||||
- Idempotenz: zweiter Lauf = No-op.
|
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).
|
||||||
## 10. Offene Entscheidungen (vor dem Bau)
|
3. ~~**Eigene Avatare**~~ — entfallen: Avatar am 28.08.2026 ausgebaut (§3·D).
|
||||||
1. ~~llama-swap systemd-Unit-Inhalt~~ → **geklärt: kompletter Unit in Anhang A** (in der Bau-Session anlegen).
|
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
||||||
2. ~~Voice-Referenz-Audio-Ort~~ → **geklärt: `~/.voice/refs/`** (Backup-Snippet in Anhang B, in der Bau-Session umsetzen).
|
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
||||||
3. **Eigene Avatare**: künftiger Upload-/Ablage-Mechanismus + Backup-Pfad (einziger offener 1:1-Daten-Punkt).
|
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
||||||
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
|
||||||
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
## 11. Referenzen
|
||||||
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
- `backend/config.py`, [[project-mc2-architecture]], [[project-stack-state]]
|
||||||
|
- [[project-hermes-setup]], `docs/HERMES_SETUP.md` (entfesseltes Profil, Browser, PC-Executor)
|
||||||
## 11. Referenzen
|
- [[voice-sprechen-feature]] (Voice + 3D-Avatar), [[mem0-memory-architecture]], [[mc2-backup-restore]]
|
||||||
- `backend/config.py`, [[project-mc2-architecture]], [[project-stack-state]]
|
- `deploy/{deploy,backup,restore,provision-engine,stack-postcheck,warmup}.sh`, `voice_service/install.sh`
|
||||||
- [[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
|
||||||
## Anhang A — `llama-swap.service` (1:1 von der Box abgegriffen, 2026-06-28)
|
(hitonabi, gfx1151 → HSA 11.5.1) — auf anderer Hardware anpassen. Die `.d/`-Drop-ins
|
||||||
|
(`vulkan.conf`, `warmup.conf`) legt `provision-engine.sh` an.
|
||||||
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
|
```ini
|
||||||
(`vulkan.conf`, `warmup.conf`) legt `provision-engine.sh` an.
|
[Unit]
|
||||||
|
Description=llama-swap (lokaler LLM Router)
|
||||||
```ini
|
After=network-online.target
|
||||||
[Unit]
|
Wants=network-online.target
|
||||||
Description=llama-swap (lokaler LLM Router)
|
|
||||||
After=network-online.target
|
[Service]
|
||||||
Wants=network-online.target
|
Type=simple
|
||||||
|
User=hitonabi
|
||||||
[Service]
|
Environment=HSA_OVERRIDE_GFX_VERSION=11.5.1
|
||||||
Type=simple
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
User=hitonabi
|
ExecStart=/usr/local/bin/llama-swap --config /etc/llama-swap/config.yaml --listen 0.0.0.0:8080 --watch-config
|
||||||
Environment=HSA_OVERRIDE_GFX_VERSION=11.5.1
|
Restart=on-failure
|
||||||
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
RestartSec=3
|
||||||
ExecStart=/usr/local/bin/llama-swap --config /etc/llama-swap/config.yaml --listen 0.0.0.0:8080 --watch-config
|
|
||||||
Restart=on-failure
|
[Install]
|
||||||
RestartSec=3
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
[Install]
|
```ini
|
||||||
WantedBy=multi-user.target
|
# /etc/systemd/system/llama-swap.service.d/vulkan.conf
|
||||||
```
|
[Service]
|
||||||
```ini
|
Environment=LD_LIBRARY_PATH=/opt/llamacpp-vulkan
|
||||||
# /etc/systemd/system/llama-swap.service.d/vulkan.conf
|
```
|
||||||
[Service]
|
```ini
|
||||||
Environment=LD_LIBRARY_PATH=/opt/llamacpp-vulkan
|
# /etc/systemd/system/llama-swap.service.d/warmup.conf
|
||||||
```
|
[Service]
|
||||||
```ini
|
ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh
|
||||||
# /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`.
|
||||||
|
|
||||||
**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}"`,
|
||||||
## Anhang B — Backup-Scope-Erweiterung für echtes 1:1 (fertiges Snippet)
|
Stage zusätzlich `"$STAGE/voice"`):
|
||||||
|
|
||||||
In `deploy/backup.sh` ergänzen (Variablen oben: `VOICE="${VOICE_HOME:-$HOME/.voice}"`,
|
```bash
|
||||||
Stage zusätzlich `"$STAGE/voice"`):
|
# 1:1-Erweiterung (scan-verifiziert 2026-06-28):
|
||||||
|
[ -f "$HERMES/token_stats.json" ] && cp -a "$HERMES/token_stats.json" "$STAGE/hermes/" || true
|
||||||
```bash
|
[ -d "$HERMES/skills" ] && cp -a "$HERMES/skills" "$STAGE/hermes/skills" || true
|
||||||
# 1:1-Erweiterung (scan-verifiziert 2026-06-28):
|
[ -d "$HERMES/sessions" ] && cp -a "$HERMES/sessions" "$STAGE/hermes/sessions" || true
|
||||||
[ -f "$HERMES/token_stats.json" ] && cp -a "$HERMES/token_stats.json" "$STAGE/hermes/" || true
|
[ -d "$VOICE/refs" ] && cp -a "$VOICE/refs" "$STAGE/voice/refs" || true
|
||||||
[ -d "$HERMES/skills" ] && cp -a "$HERMES/skills" "$STAGE/hermes/skills" || true
|
```
|
||||||
[ -d "$HERMES/sessions" ] && cp -a "$HERMES/sessions" "$STAGE/hermes/sessions" || true
|
tar-Aufruf um den re-downloadbaren Skill-Cache erleichtern:
|
||||||
[ -d "$VOICE/refs" ] && cp -a "$VOICE/refs" "$STAGE/voice/refs" || true
|
```bash
|
||||||
```
|
tar --exclude='./hermes/skills/.hub' -czf "$OUT" -C "$STAGE" .
|
||||||
tar-Aufruf um den re-downloadbaren Skill-Cache erleichtern:
|
```
|
||||||
```bash
|
In `deploy/restore.sh` spiegelbildlich (skills/sessions **mergen**, nicht ersetzen) und
|
||||||
tar --exclude='./hermes/skills/.hub' -czf "$OUT" -C "$STAGE" .
|
`voice-service` mit neustarten:
|
||||||
```
|
```bash
|
||||||
In `deploy/restore.sh` spiegelbildlich (skills/sessions **mergen**, nicht ersetzen) und
|
VOICE="${VOICE_HOME:-$HOME/.voice}"
|
||||||
`voice-service` mit neustarten:
|
SERVICES="mem0-service voice-service mission-control-2 hermes-gateway"
|
||||||
```bash
|
[ -f "$STAGE/hermes/token_stats.json" ] && cp -a "$STAGE/hermes/token_stats.json" "$HERMES/"
|
||||||
VOICE="${VOICE_HOME:-$HOME/.voice}"
|
[ -d "$STAGE/hermes/skills" ] && { mkdir -p "$HERMES/skills"; cp -a "$STAGE/hermes/skills/." "$HERMES/skills/"; }
|
||||||
SERVICES="mem0-service voice-service mission-control-2 hermes-gateway"
|
[ -d "$STAGE/hermes/sessions" ] && { mkdir -p "$HERMES/sessions"; cp -a "$STAGE/hermes/sessions/." "$HERMES/sessions/"; }
|
||||||
[ -f "$STAGE/hermes/token_stats.json" ] && cp -a "$STAGE/hermes/token_stats.json" "$HERMES/"
|
[ -d "$STAGE/voice/refs" ] && { mkdir -p "$VOICE/refs"; cp -a "$STAGE/voice/refs/." "$VOICE/refs/"; }
|
||||||
[ -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/"; }
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -333,3 +333,86 @@ git remote set-url origin http://192.168.178.153:3000/Hitonabi/mission-control-v
|
|||||||
```
|
```
|
||||||
|
|
||||||
Noch **nicht** gemacht — aendert Git-Konfiguration in den Repos des Users.
|
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.
|
||||||
|
|||||||
@@ -62,6 +62,24 @@ keine zweite Liste anlegen. Verdikte werden NICHT als „offen" geführt → [VE
|
|||||||
|
|
||||||
## Lucy
|
## Lucy
|
||||||
|
|
||||||
|
- **Raphael-Umbau (04.09.2026) — zwei Branches offen, Reihenfolge zählt (Branch → Ampel →
|
||||||
|
User-Merge → deploy.sh bzw. `npm run dist`, NICHT übers Auftragsbuch):** 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 = Leiche im Code (Fund 04.09.2026):** Router/View/Annahme-Skripte liegen noch in
|
||||||
|
main, die Box listet Branches als Karten, aber letzte Annahme 02.08. (revertiert), Kanban-Tools
|
||||||
|
seit 21.08. aus, PC-Executor-IP in der Box veraltet (`.98` statt `.22`), STACK.md nennt den Weg
|
||||||
|
trotzdem „Standard". Entscheid nötig: ausbauen + STACK.md/GRENZEN.md auf den manuellen Weg.
|
||||||
|
- **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.
|
||||||
|
- **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;
|
- **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.
|
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,
|
Danach: Lexikon-Injektion in kokoro_server + Modell-Tausch. **pocket bleibt Default,
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
**Aufräum-Kandidat (User-Entscheid):** Auftragsbuch-Router + View + Annahme-Skripte + PC-Executor-
|
||||||
|
Verweis ausbauen und STACK.md „Deploy & Pipeline" auf den manuellen Weg umschreiben — sonst
|
||||||
|
behauptet die Doku weiter einen Standard-Weg, den niemand mehr geht.
|
||||||
|
|
||||||
|
## 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 |
|
| [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 |
|
| [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 |
|
| [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?" |
|
| [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),
|
Dazu im Repo-Wurzelverzeichnis bzw. docs/: `AGENTS.md` (verbindliche Projekt-Regeln),
|
||||||
|
|||||||
+21
-13
@@ -43,30 +43,38 @@ diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
|||||||
|
|
||||||
## Modell-Stack (llama-swap, live 19.08.2026)
|
## 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.
|
Qwen3.6-35B-A3B + Qwen3-Embedding-0.6B + Qwen3-Reranker-0.6B. Sonst NICHTS dauerhaft warm.
|
||||||
|
|
||||||
| Alias/Rolle | Modell | ttl | Notizen |
|
| 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 |
|
| `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) |
|
| `coder` | Qwen3.8-27B (Q4_K_M, mmproj BF16) | 5400 | **131072 ctx** — `--parallel 1`, der Coder bekommt den ganzen Slot (34a9862). Multimodal, ~12,7 t/s (dicht = bandbreitengebunden) |
|
||||||
| `debugger` / `doctor` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65k ctx; Runtime-Diagnostik & Fehler-Debugger (Multimodal) |
|
| `debugger` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65536 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-Instruct (Q4_K_M) | 900 | 32768 ctx; Multimodal-Augen (On-Demand) |
|
||||||
| `vision` | Qwen3-VL-30B-A3B (Q4_K_M) | 900 | 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 |
|
||||||
| `scout` | GLM-4.6V-Flash (Q4_K_M) | 300 | Schneller Tool- und Vision-Allrounder |
|
| `reranker` | Qwen3-Reranker-0.6B (q8_0, Mungert) | 0 | Sortiert Suchtreffer nach echter Relevanz (`/v1/rerank`) |
|
||||||
| `heavy` | gpt-oss-120b (mxfp4) | 600 | 32k ctx; Chef-Gutachter (nur nachts / gezielter On-Demand Call) |
|
| `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Suche/Sortierung |
|
||||||
| `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`) |
|
> **Diese sieben sind ALLES** — abgeglichen mit `/etc/llama-swap/config.yaml` am 27.08.2026.
|
||||||
| `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Chroma / Mem0 |
|
> 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).
|
Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben).
|
||||||
|
|
||||||
## Hermes (Agent)
|
## Hermes (Agent)
|
||||||
|
|
||||||
- **v0.20.4** (git-Install `~/.hermes/hermes-agent`), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
- **v0.20.6** (2026.8.27, 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`).
|
- **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: smart`, `approvals.cron_mode: deny`.
|
- `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.
|
- 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)
|
## Automatik-Fahrplan (was nachts von allein läuft)
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ planen, **jede Session hinterlässt einen sauberen Stand**.
|
|||||||
Antigravity liest den F:\-Checkout — versioniert + deploybar). Der Vault bleibt Lucys
|
Antigravity liest den F:\-Checkout — versioniert + deploybar). Der Vault bleibt Lucys
|
||||||
Lernschicht.
|
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).
|
||||||
|
|
||||||
## Recherche-Fundament (10.07., Web)
|
## Recherche-Fundament (10.07., Web)
|
||||||
|
|
||||||
Das Industrie-Muster 2026 ist exakt unsere Architektur: Issue → Agent → PR → Mensch-Gate, nie
|
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
+6
-6
File diff suppressed because one or more lines are too long
-31
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
+6
-6
File diff suppressed because one or more lines are too long
+35
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-CCbyojll.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
import{u,ag as m,k as x,j as s,y as n,ah as b,b as p,n as f,q as h}from"./index-l6kyKCL9.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-l6kyKCL9.js";import{E as k}from"./external-link-ClFd8iN1.js";import{R as y}from"./refresh-cw-BV1qZMSR.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};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-CCbyojll.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
|
||||||
File diff suppressed because one or more lines are too long
+16
-16
File diff suppressed because one or more lines are too long
@@ -1,6 +0,0 @@
|
|||||||
import{c as s,j as e}from"./index-CCbyojll.js";/**
|
|
||||||
* @license lucide-react v0.460.0 - ISC
|
|
||||||
*
|
|
||||||
* This source code is licensed under the ISC license.
|
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
|
||||||
*/const r=s("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);function c({icon:a,children:t}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(a,{className:"h-3.5 w-3.5"})," ",t]})}export{r as H,c as S};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{j as e}from"./index-l6kyKCL9.js";function n({icon:t,children:s}){return e.jsxs("p",{className:"flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(t,{className:"h-3.5 w-3.5"})," ",s]})}export{n as S};
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
var M=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||M("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?M("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a9 as B,aa as F,ab as E,ac as I,ad as P,u as A,r as f,ae as J,af as Q,c as T,ag as U,j as s,G as D,T as H,b as K,L,M as G,q as V,n as R}from"./index-CCbyojll.js";import{J as _}from"./JobsBar-Ckecoyqy.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
var E=r=>{throw TypeError(r)};var O=(r,e,t)=>e.has(r)||E("Cannot "+t);var a=(r,e,t)=>(O(r,e,"read from private field"),t?t.call(r):e.get(r)),y=(r,e,t)=>e.has(r)?E("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),v=(r,e,t,n)=>(O(r,e,"write to private field"),n?n.call(r,t):e.set(r,t),t),j=(r,e,t)=>(O(r,e,"access private method"),t);import{a8 as Q,a9 as A,aa as M,ab as B,ac as P,u as J,r as f,ad as F,ae as I,c as T,af as U,j as s,J as D,T as H,b as K,L,Q as V,q as _,n as R}from"./index-l6kyKCL9.js";import{J as z}from"./JobsBar-HflSaqor.js";var c,g,o,h,m,w,C,q,G=(q=class extends Q{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),A(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&M(t.mutationKey)!==M(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??B();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=J(),[n]=f.useState(()=>new G(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(F)},[n]);if(l.error&&I(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
*/const X=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);function ee(){const r=A(),{data:e,isLoading:t,isError:n}=U({queryKey:["skills-list"],queryFn:()=>R("/api/skills"),refetchInterval:5e3}),[l,x]=f.useState(null),[d,p]=f.useState(null),k=W({mutationFn:i=>R("/api/skills/run",{method:"POST",body:JSON.stringify({skill_name:i})}),onMutate:i=>{x(i),p(null)},onSuccess:(i,u)=>{x(null),p({name:u,ok:i.ok,msg:i.msg||i.err||"Unbekannter Fehler"}),r.invalidateQueries({queryKey:V.jobs}),r.invalidateQueries({queryKey:["skills-list"]})},onError:(i,u)=>{x(null),p({name:u,ok:!1,msg:String(i)})}});return s.jsxs("div",{className:"space-y-6",children:[s.jsx("div",{className:"flex flex-col sm:flex-row justify-between sm:items-center gap-4",children:s.jsxs("div",{children:[s.jsx("h1",{className:"text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent",children:"Skills & Jobs"}),s.jsx("p",{className:"text-sm text-muted-foreground",children:"Löst autonome Agent-Skills manuell auf der Box aus. Die Ausführung erfolgt im Hintergrund durch Hermes."})]})}),s.jsx(_,{}),d&&s.jsxs("div",{className:K("p-4 rounded-lg border flex gap-3 items-start",d.ok?"bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400":"bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400"),children:[d.ok?s.jsx(D,{className:"h-5 w-5 mt-0.5 shrink-0"}):s.jsx(H,{className:"h-5 w-5 mt-0.5 shrink-0"}),s.jsxs("div",{children:[s.jsx("div",{className:"font-semibold text-sm",children:d.name}),s.jsx("div",{className:"text-sm opacity-90",children:d.msg})]})]}),t&&s.jsx("div",{className:"flex h-32 items-center justify-center",children:s.jsx(L,{className:"h-6 w-6 animate-spin text-muted-foreground"})}),n&&s.jsx("div",{className:"text-red-500 p-4 mc-card text-sm",children:"Fehler beim Laden der Skills. Ist das Backend erreichbar?"}),(e==null?void 0:e.skills)&&s.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",children:[e.skills.map(i=>s.jsxs("div",{className:"mc-card p-5 flex flex-col hover:border-primary/40 transition-colors group",children:[s.jsxs("div",{className:"flex items-start justify-between gap-3 mb-2",children:[s.jsxs("div",{className:"flex items-center gap-2 font-semibold text-base font-space",children:[s.jsx(G,{className:"h-5 w-5 text-primary/80"}),i.name]}),s.jsxs("button",{onClick:()=>k.mutate(i.name),disabled:l===i.name||i.running||k.isPending,className:K("shrink-0 h-8 px-3 rounded-md text-xs font-semibold uppercase tracking-wider flex items-center gap-1.5 transition-all cursor-pointer",l===i.name||i.running?"bg-sky-500/10 border border-sky-500/40 text-sky-400 cursor-not-allowed":"bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground group-hover:shadow-md group-hover:shadow-primary/20"),children:[l===i.name||i.running?s.jsx(L,{className:"h-3.5 w-3.5 animate-spin"}):s.jsx(X,{className:"h-3.5 w-3.5"}),l===i.name||i.running?"Läuft …":"Starten"]})]}),s.jsx("div",{className:"text-sm text-muted-foreground flex-1",children:i.description})]},i.name)),e.skills.length===0&&s.jsxs("div",{className:"col-span-full p-8 text-center text-muted-foreground mc-card border-dashed border-2",children:["Keine Skills im Ordner ",s.jsx("code",{className:"text-xs text-foreground bg-muted px-1.5 py-0.5 rounded",children:"deploy/skills"})," gefunden."]})]})]})}export{ee as SkillsView};
|
*/const X=T("Play",[["polygon",{points:"6 3 20 12 6 21 6 3",key:"1oa8hb"}]]);function ee(){const r=J(),{data:e,isLoading:t,isError:n}=U({queryKey:["skills-list"],queryFn:()=>R("/api/skills"),refetchInterval:5e3}),[l,x]=f.useState(null),[d,p]=f.useState(null),k=W({mutationFn:i=>R("/api/skills/run",{method:"POST",body:JSON.stringify({skill_name:i})}),onMutate:i=>{x(i),p(null)},onSuccess:(i,u)=>{x(null),p({name:u,ok:i.ok,msg:i.msg||i.err||"Unbekannter Fehler"}),r.invalidateQueries({queryKey:_.jobs}),r.invalidateQueries({queryKey:["skills-list"]})},onError:(i,u)=>{x(null),p({name:u,ok:!1,msg:String(i)})}});return s.jsxs("div",{className:"space-y-6",children:[s.jsx("div",{className:"flex flex-col sm:flex-row justify-between sm:items-center gap-4",children:s.jsxs("div",{children:[s.jsx("h1",{className:"text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent",children:"Skills & Jobs"}),s.jsx("p",{className:"text-sm text-muted-foreground",children:"Löst autonome Agent-Skills manuell auf der Box aus. Die Ausführung erfolgt im Hintergrund durch Hermes."})]})}),s.jsx(z,{}),d&&s.jsxs("div",{className:K("p-4 rounded-lg border flex gap-3 items-start",d.ok?"bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400":"bg-red-500/10 border-red-500/20 text-red-600 dark:text-red-400"),children:[d.ok?s.jsx(D,{className:"h-5 w-5 mt-0.5 shrink-0"}):s.jsx(H,{className:"h-5 w-5 mt-0.5 shrink-0"}),s.jsxs("div",{children:[s.jsx("div",{className:"font-semibold text-sm",children:d.name}),s.jsx("div",{className:"text-sm opacity-90",children:d.msg})]})]}),t&&s.jsx("div",{className:"flex h-32 items-center justify-center",children:s.jsx(L,{className:"h-6 w-6 animate-spin text-muted-foreground"})}),n&&s.jsx("div",{className:"text-red-500 p-4 mc-card text-sm",children:"Fehler beim Laden der Skills. Ist das Backend erreichbar?"}),(e==null?void 0:e.skills)&&s.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4",children:[e.skills.map(i=>s.jsxs("div",{className:"mc-card p-5 flex flex-col hover:border-primary/40 transition-colors group",children:[s.jsxs("div",{className:"flex items-start justify-between gap-3 mb-2",children:[s.jsxs("div",{className:"flex items-center gap-2 font-semibold text-base font-space",children:[s.jsx(V,{className:"h-5 w-5 text-primary/80"}),i.name]}),s.jsxs("button",{onClick:()=>k.mutate(i.name),disabled:l===i.name||i.running||k.isPending,className:K("shrink-0 h-8 px-3 rounded-md text-xs font-semibold uppercase tracking-wider flex items-center gap-1.5 transition-all cursor-pointer",l===i.name||i.running?"bg-sky-500/10 border border-sky-500/40 text-sky-400 cursor-not-allowed":"bg-primary/10 text-primary hover:bg-primary hover:text-primary-foreground group-hover:shadow-md group-hover:shadow-primary/20"),children:[l===i.name||i.running?s.jsx(L,{className:"h-3.5 w-3.5 animate-spin"}):s.jsx(X,{className:"h-3.5 w-3.5"}),l===i.name||i.running?"Läuft …":"Starten"]})]}),s.jsx("div",{className:"text-sm text-muted-foreground flex-1",children:i.description})]},i.name)),e.skills.length===0&&s.jsxs("div",{className:"col-span-full p-8 text-center text-muted-foreground mc-card border-dashed border-2",children:["Keine Skills im Ordner ",s.jsx("code",{className:"text-xs text-foreground bg-muted px-1.5 py-0.5 rounded",children:"deploy/skills"})," gefunden."]})]})]})}export{ee as SkillsView};
|
||||||
+42
File diff suppressed because one or more lines are too long
+19
-14
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as r}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const o=r("ArrowRight",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);export{o as A};
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as o}from"./index-CCbyojll.js";/**
|
import{c as o}from"./index-l6kyKCL9.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import{c}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const o=c("Clock",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["polyline",{points:"12 6 12 12 16 14",key:"68esgv"}]]);export{o as C};
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import{c as a}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const c=a("Camera",[["path",{d:"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z",key:"1tc9qg"}],["circle",{cx:"12",cy:"13",r:"3",key:"1vg3eu"}]]);/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const o=a("CloudDownload",[["path",{d:"M12 13v8l-4-4",key:"1f5nwf"}],["path",{d:"m12 21 4-4",key:"1lfcce"}],["path",{d:"M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284",key:"ui1hmy"}]]);export{o as C,c as a};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-CCbyojll.js";/**
|
import{c as e}from"./index-l6kyKCL9.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as a}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const e=a("ExternalLink",[["path",{d:"M15 3h6v6",key:"1q9fwt"}],["path",{d:"M10 14 21 3",key:"gplh6r"}],["path",{d:"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6",key:"a6xqqp"}]]);export{e as E};
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as e}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const t=e("FileText",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]]);export{t as F};
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import{c as a}from"./index-CCbyojll.js";/**
|
|
||||||
* @license lucide-react v0.460.0 - ISC
|
|
||||||
*
|
|
||||||
* This source code is licensed under the ISC license.
|
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
|
||||||
*/const o=a("FolderOpen",[["path",{d:"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",key:"usdka0"}]]);export{o as F};
|
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as a}from"./index-l6kyKCL9.js";/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const m=a("Hammer",[["path",{d:"m15 12-8.373 8.373a1 1 0 1 1-3-3L12 9",key:"eefl8a"}],["path",{d:"m18 15 4-4",key:"16gjal"}],["path",{d:"m21.5 11.5-1.914-1.914A2 2 0 0 1 19 8.172V7l-2.26-2.26a6 6 0 0 0-4.202-1.756L9 2.96l.92.82A6.18 6.18 0 0 1 12 8.4V10l2 2h1.172a2 2 0 0 1 1.414.586L18.5 14.5",key:"b7pghm"}]]);export{m as H};
|
||||||
-435
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
import{aA as r}from"./index-l6kyKCL9.js";var o=r();export{o as r};
|
||||||
+1
File diff suppressed because one or more lines are too long
+269
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user