Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
041ede7f8d | ||
|
|
33032f8fb0 | ||
|
|
3249c8e942 | ||
|
|
24a0694b80 | ||
|
|
a494d320d7 | ||
|
|
7957cda438 | ||
|
|
dcee0096fe | ||
|
|
b74e98ffbb | ||
|
|
5aa5a484a7 | ||
|
|
464b0d20b8 | ||
|
|
ae69c5ce31 | ||
|
|
8aa22e6591 | ||
|
|
3d1881f4bc | ||
|
|
2935752613 | ||
|
|
a969898a16 | ||
|
|
57492d6759 | ||
|
|
ba9ea7743f | ||
|
|
84dc34c0a3 | ||
|
|
34a9862591 | ||
|
|
f3139d2b5d | ||
|
|
6e573d5551 | ||
|
|
2210bf5c40 | ||
|
|
dc78114102 | ||
|
|
259e1b2ca8 | ||
|
|
148a039545 | ||
|
|
25dbfb2b61 | ||
|
|
fa69edc1aa | ||
|
|
9cf04c427e | ||
|
|
f446da8ae6 | ||
|
|
c2a54bd25d | ||
|
|
56668ee43d | ||
|
|
c8e5a7162b | ||
|
|
00b64b9fdc | ||
|
|
7c23ebf382 | ||
|
|
e04ace242c | ||
|
|
d880a76b6a | ||
|
|
ff319ff291 | ||
|
|
6e3440379b | ||
|
|
5f35f0d580 | ||
|
|
43fa747bc3 | ||
|
|
83c6ecc613 | ||
|
|
b316ad40ba | ||
|
|
8c371b946b | ||
|
|
1a2051e988 | ||
|
|
5d93822a7e |
@@ -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": [
|
||||||
|
|||||||
+22
-4
@@ -18,18 +18,36 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5180", "--strictPort"],
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
"--",
|
||||||
|
"--port",
|
||||||
|
"5180",
|
||||||
|
"--strictPort"
|
||||||
|
],
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||||
"env": { "MC_API_TARGET": "http://192.168.178.151:9001" },
|
"env": {
|
||||||
|
"MC_API_TARGET": "http://192.168.178.151:9001"
|
||||||
|
},
|
||||||
"autoPort": false,
|
"autoPort": false,
|
||||||
"port": 5180
|
"port": 5180
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "frontend-mock",
|
"name": "frontend-mock",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "dev", "--", "--port", "5181", "--strictPort"],
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
"--",
|
||||||
|
"--port",
|
||||||
|
"5181",
|
||||||
|
"--strictPort"
|
||||||
|
],
|
||||||
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
"cwd": "F:\\Coding Stuff\\mission-control-2\\frontend",
|
||||||
"env": { "MC_API_TARGET": "http://127.0.0.1:9000" },
|
"env": {
|
||||||
|
"MC_API_TARGET": "http://127.0.0.1:9000"
|
||||||
|
},
|
||||||
"autoPort": false,
|
"autoPort": false,
|
||||||
"port": 5181
|
"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]
|
||||||
|
|
||||||
@@ -43,6 +44,64 @@ jobs:
|
|||||||
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"
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ __pycache__/
|
|||||||
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.
|
|
||||||
# Wird per Direkt-Deploy auf die Box gespielt (dist/avatar.vrm), nicht über git.
|
|
||||||
frontend/public/avatar.vrm
|
|
||||||
frontend/dist/avatar.vrm
|
|
||||||
|
|
||||||
# Env / local
|
# Env / local
|
||||||
*.env
|
*.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
|
|||||||
- **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,
|
||||||
@@ -24,7 +24,6 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
|
|||||||
- **`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.
|
||||||
@@ -48,6 +47,14 @@ Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI +
|
|||||||
- **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
|
||||||
|
`hitonabi ALL=(ALL) NOPASSWD: ALL` — der Nutzer, unter dem alle MC2-Dienste laufen, darf ohnehin
|
||||||
|
alles passwortlos. `sudoers.d/mc2-autonomie` und `sudoers.d/mission-control` dokumentieren also,
|
||||||
|
was gebraucht *würde*, schränken aber nichts ein. Das ist eine bewusste Entscheidung für die
|
||||||
|
Single-User-Appliance; verlasse dich beim Bauen nicht darauf, dass eine Whitelist dich bremst.
|
||||||
|
Wer das wirklich härten will, kommt um einen eigenen Service-User nicht herum — die Pauschalzeile
|
||||||
|
einfach zu ziehen, bricht OS-Update, Config-Sync und den wöchentlichen Auto-Neustart still.
|
||||||
|
(Geprüft 27.08.2026; die doppelte Zeile wurde damals entfernt, Sicherung `/root/sudoers.bak-*`.)
|
||||||
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
- Lokal (Windows) müssen Box-Shell-Befehle **harmlos fehlschlagen** statt zu crashen.
|
||||||
|
|
||||||
## Grenzen (Verdikt, eingehalten)
|
## Grenzen (Verdikt, eingehalten)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+42
-11
@@ -1,6 +1,7 @@
|
|||||||
# Savepoint — Mission Control 2.0 (MC2)
|
# Savepoint — Mission Control 2.0 (MC2)
|
||||||
|
|
||||||
_Stand: 2026-08-19. Zustands-Snapshot nach Stack-Vollupdate (Hermes v0.20.4, llama.cpp b10502, llama-swap v250, Qwen 3.8 DFlash-2 Vorbereitung & Doku-Bereinigung)._
|
_Stand: 2026-08-20, nach der Aufräum-Runde. Alle Zahlen hier sind **auf der Box gemessen**, nicht
|
||||||
|
aus Doku übernommen._
|
||||||
|
|
||||||
## Was das ist (in einem Satz)
|
## Was das ist (in einem Satz)
|
||||||
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
||||||
@@ -12,15 +13,45 @@ Agenten-Status, Updates & Wartung — und dient als Steuerpult für die autonome
|
|||||||
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
||||||
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
|
(Box hat kein Node; Backend liefert die gebauten Assets direkt aus).
|
||||||
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
|
- **MC2-Gateway** `:9010` (`/v1`-Datenpfad, model:auto Routing + native Bildweiche).
|
||||||
- **Modell-Router** `:8080` (llama-swap, Vulkan/RADV Engine b10502).
|
- **Modell-Router** `:8080` (llama-swap v250, Vulkan/RADV Engine b10502).
|
||||||
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4 mit nativem Bot-Mode).
|
- **Hermes Agent** `:8642` (`hermes-gateway`, v0.20.4) + `hermes dashboard` auf `:9119` (nur Loopback).
|
||||||
- **Sidecars:** `mem0_service/` (:8765, semantischer Chroma-Gedächtnis-Sidecar), `voice_service/` (:8650, faster-whisper STT + Piper TTS), `mcp/` (MCP-Server), `client/hermes-pc` (PC-Executor :7777).
|
- **Sidecars:** `voice_service/` (:8650, faster-whisper STT + Piper TTS), `mcp/` (4 MCP-Server),
|
||||||
|
`client/hermes-pc` (PC-Executor :7777).
|
||||||
|
‼️ `mem0_service/` liegt noch im Baum, ist aber **stillgelegt** — `:8765` antwortet nicht.
|
||||||
|
|
||||||
## Aktueller Zustand (19.08.2026)
|
## Modelle live (gemessen 20.08.2026)
|
||||||
- **Stack-Vollupdate abgeschlossen:** llama-swap v250, llama.cpp b10502 (Vulkan), Hermes Agent v0.20.4 (Bot-Mode Support).
|
| Rolle (Alias) | Modell | Tempo | Zustand |
|
||||||
- **Selbsttests:** `self-smoke.sh` zu 100 % grün.
|
|---|---|---|---|
|
||||||
- **Codebase-Bereinigung:** Verwaiste Governor-Schnittstellen im Frontend entfernt, Regex-Versionsparsing in `maintenance.py` gefixt, Doku-Wahrheit (Mem0-Sidecar, Modelltabellen, Ports) wiederhergestellt.
|
| `hermes` / `fast` | Qwen3.6-35B-A3B (MoE, DFlash) | **95,7 t/s** · Prompt 221 t/s | immer warm |
|
||||||
- **Modelle live:** hermes=Qwen3.6-35B-A3B (immer warm, ~70–90 t/s) · coder=Qwen3-Coder-Next (51,5 t/s) · debugger=Muse-Glimmer-30B · kritiker=Devstral-Small-2-24B (16k Deckel) · dense-planer=Qwen3.8-27B · heavy=gpt-oss-120b · vision=Qwen3-VL-30B · scout=GLM-4.6V · embed/reranker=Qwen3 0.6B.
|
| `coder` | Qwen3.8-27B (dicht, Vision) | **12,6 t/s** · Prompt 144 t/s | ttl 5400, ~29 s Kaltstart |
|
||||||
|
| `debugger` | Muse-Glimmer-30B (DFlash) | — | ttl 600 |
|
||||||
|
| `heavy` | gpt-oss-120b | — | on-demand |
|
||||||
|
| `vision` | Qwen3-VL-30B-A3B-Instruct | — | on-demand |
|
||||||
|
| `embed` / `reranker` | Qwen3-Embedding-0.6B / Qwen3-Reranker-0.6B | — | immer warm |
|
||||||
|
|
||||||
## Letzter Sicherungspunkt
|
Warm-Gruppe `brains` = embed + reranker + Qwen3.6-35B-A3B. Warm-Wächter weckt nur `fast`.
|
||||||
- Stand: 19.08.2026 nach Stack-Audit & Cleanup.
|
|
||||||
|
**Hintergrund zum Tempo:** Strix Halo liefert ~215 GB/s Speicherbandbreite. Ein *dichtes* Modell
|
||||||
|
muss pro Token seine vollen Gewichte lesen → 17 GB ÷ 215 GB/s ≈ 12,6 t/s ist das **Hardware-Limit**,
|
||||||
|
kein Konfigurationsfehler. MoE-Modelle lesen pro Token nur einen Bruchteil und sind darum ~8× schneller.
|
||||||
|
|
||||||
|
## Aufgeräumt am 20.08.2026
|
||||||
|
- CI-Ampel wieder **grün** (Lauf #77) — zwei ungenutzte `MEM0_SERVICE_URL`-Importe entfernt.
|
||||||
|
- Kaputter systemd-Timer `mc2-morgen-digest` abgeschaltet (der Hermes-Cron macht die Arbeit).
|
||||||
|
- `mcp-stderr.log` (27 MB, 238k Ping-Zeilen) geleert.
|
||||||
|
- Config-Drift aufgelöst: Repo == `/etc/llama-swap/config.yaml`. Warm-Set-Idee geparkt auf
|
||||||
|
Branch `parked/warm-set-5d93822`.
|
||||||
|
- Entfernt: Governor-Reste, OpenCode-/aider-Reste (853 MB), 61 alte Backup-Dateien,
|
||||||
|
verwaiste Modelle Devstral-Small-2 + GLM-4.6V-Flash + mem0 (21,5 GB) → `/srv/models` 209 → 189 GB.
|
||||||
|
- **Fähigkeiten repariert:** `orchestrator`- und `wartung`-Skill sowie `fremdblick.sh` zeigten auf
|
||||||
|
gelöschte Modelle (`Qwen3-Coder-Next`, `GLM-4.6V-Flash`, `GLM-4.7-Flash`) → der Kritiker-Gate war
|
||||||
|
stumm kaputt. Jetzt auf **Rollen-Aliase** umgestellt, damit Modellwechsel sie nicht mehr brechen.
|
||||||
|
- Alles Gelöschte liegt gesichert in `~/archiv-aufraeumen-20260820/` (11 MB).
|
||||||
|
|
||||||
|
## Offen
|
||||||
|
- `mem0_service/` + `_mem0_reachable()` in `backend/routers/system.py` ausbauen (toter Port).
|
||||||
|
- `~/.hermes/scripts/fremdblick.sh` ist **unversioniert** — gehört ins Repo.
|
||||||
|
- MCP-Log dauerhaft deckeln (Hermes' `max_size_mb` greift für gekapertes stderr nicht).
|
||||||
|
- Verschwundene Wächter: Ampel-Wächter, Prüfstand, Stack-Rundumschlag.
|
||||||
|
- Coder-Tempo: Entwurfsmodelle für spekulatives Dekodieren liegen ungenutzt unter
|
||||||
|
`/srv/models/Qwen3.8-27B-DFlash2-GGUF/`.
|
||||||
|
|||||||
+12
-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,7 +93,8 @@ 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("/")
|
||||||
# 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
|
||||||
@@ -108,8 +105,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()
|
||||||
|
|||||||
+77
-29
@@ -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()
|
alt = _fingerprints()
|
||||||
yield ": verbunden\n\n"
|
yield ": verbunden\n\n"
|
||||||
|
|
||||||
|
seit_abdruck = 0.0
|
||||||
seit_ping = 0.0
|
seit_ping = 0.0
|
||||||
|
takt = METRIK_S if mit_metrik else ABDRUCK_S
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if await request.is_disconnected():
|
if await request.is_disconnected():
|
||||||
return
|
return
|
||||||
await asyncio.sleep(TICK_S)
|
await asyncio.sleep(takt)
|
||||||
seit_ping += TICK_S
|
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:
|
|
||||||
|
if seit_ping >= KEEPALIVE_S:
|
||||||
yield ": ping\n\n"
|
yield ": ping\n\n"
|
||||||
seit_ping = 0.0
|
seit_ping = 0.0
|
||||||
|
|
||||||
return StreamingResponse(strom(), media_type="text/event-stream",
|
|
||||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
_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)
|
||||||
|
try:
|
||||||
data = r.json()
|
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):
|
||||||
|
# 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()
|
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.
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+10
-19
@@ -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.
|
||||||
@@ -10,7 +16,7 @@ die häufigste Fehlerquelle. Der Aufrufer übergibt den Host explizit.
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, MEM0_SERVICE_URL, PORT, V1_UPSTREAM
|
from config import HERMES_BUILTIN_UI_UPSTREAM, LLAMA_SWAP_URL, PORT, V1_UPSTREAM
|
||||||
|
|
||||||
DEFAULT_HOST = "192.168.178.151"
|
DEFAULT_HOST = "192.168.178.151"
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -546,59 +546,59 @@ def update_details(kind: str) -> dict:
|
|||||||
"hermes": hermes_update_details}.get(kind, lambda: {"error": "unbekannt"})()
|
"hermes": hermes_update_details}.get(kind, lambda: {"error": "unbekannt"})()
|
||||||
|
|
||||||
|
|
||||||
def _run(cmd: list[str], sudo_password: str | None = None) -> dict:
|
def _run(cmd: list[str]) -> dict:
|
||||||
actual_cmd = list(cmd)
|
"""Befehl ausfuehren. sudo laeuft IMMER mit `-n` (never prompt).
|
||||||
has_sudo = False
|
|
||||||
|
|
||||||
|
v3-Umbau P1 (28.08.2026): Frueher konnte hier ein Sudo-Passwort durchgereicht und per
|
||||||
|
`-S` an stdin gefuettert werden — das Passwort kam aus dem `localStorage` des Browsers
|
||||||
|
und reiste bei jedem mutierenden Request mit. Auf der Box gemessen: `sudo -n true`
|
||||||
|
laeuft durch, weil `/etc/sudoers` den Dienst-Nutzer mit `NOPASSWD: ALL` fuehrt. Der
|
||||||
|
ganze Pfad war also totes Risiko ohne Gegenwert.
|
||||||
|
|
||||||
|
`-n` heisst: Braucht sudo je doch ein Passwort, scheitert der Befehl SOFORT und sauber,
|
||||||
|
statt auf eine Eingabe zu warten, die es hier nicht gibt. Das meldet die Funktion
|
||||||
|
darunter als `password_required` — ein ehrliches Konfigurations-Signal, das man auf der
|
||||||
|
Box loest und nicht mit einem im Browser geparkten Geheimnis uebertuencht."""
|
||||||
|
actual_cmd = list(cmd)
|
||||||
if cmd and cmd[0] == "sudo":
|
if cmd and cmd[0] == "sudo":
|
||||||
has_sudo = True
|
|
||||||
# If we have a password, use -S instead of -n
|
|
||||||
if sudo_password is not None:
|
|
||||||
if "-n" in actual_cmd:
|
|
||||||
actual_cmd = [x for x in actual_cmd if x != "-n"]
|
|
||||||
if "-S" not in actual_cmd:
|
|
||||||
actual_cmd.insert(1, "-S")
|
|
||||||
else:
|
|
||||||
# Force -n to fail cleanly if password is required
|
|
||||||
if "-S" in actual_cmd:
|
if "-S" in actual_cmd:
|
||||||
actual_cmd = [x for x in actual_cmd if x != "-S"]
|
actual_cmd = [x for x in actual_cmd if x != "-S"]
|
||||||
if "-n" not in actual_cmd:
|
if "-n" not in actual_cmd:
|
||||||
actual_cmd.insert(1, "-n")
|
actual_cmd.insert(1, "-n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
input_data = (sudo_password + "\n") if (has_sudo and sudo_password is not None) else None
|
p = subprocess.run(actual_cmd, capture_output=True, text=True, timeout=120)
|
||||||
p = subprocess.run(actual_cmd, input=input_data, capture_output=True, text=True, timeout=120)
|
|
||||||
|
|
||||||
err_msg = p.stderr or ""
|
err_msg = p.stderr or ""
|
||||||
if p.returncode != 0 and ("a password is required" in err_msg or "password" in err_msg.lower() or "sudo:" in err_msg):
|
if p.returncode != 0 and ("a password is required" in err_msg or "password" in err_msg.lower() or "sudo:" in err_msg):
|
||||||
if sudo_password is not None:
|
return {"ok": False, "status": "password_required", "out": p.stdout or "",
|
||||||
return {"ok": False, "status": "incorrect_password", "out": p.stdout or "", "err": "Falsches Sudo-Passwort."}
|
"err": "sudo verlangt hier ein Passwort. Das ist eine Konfigurations-Frage "
|
||||||
return {"ok": False, "status": "password_required", "out": p.stdout or "", "err": "Sudo-Passwort erforderlich."}
|
"auf der Box (sudoers), nichts, was die Oberflaeche liefern kann."}
|
||||||
|
|
||||||
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
|
return {"ok": p.returncode == 0, "out": (p.stdout or "")[-4000:], "err": (p.stderr or "")[-2000:]}
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {"ok": False, "out": "", "err": str(exc)}
|
return {"ok": False, "out": "", "err": str(exc)}
|
||||||
|
|
||||||
|
|
||||||
def check_sudo_needs_password(sudo_password: str | None = None) -> dict | None:
|
def check_sudo_needs_password() -> dict | None:
|
||||||
"""Checks if sudo needs a password. Returns error dict if password required/incorrect, else None."""
|
"""Laeuft sudo hier passwortlos? Fehler-Dict wenn nein, sonst None."""
|
||||||
res = _run(["sudo", "true"], sudo_password=sudo_password)
|
res = _run(["sudo", "true"])
|
||||||
if not res["ok"]:
|
if not res["ok"]:
|
||||||
return res
|
return res
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def restart_service(name: str, sudo_password: str | None = None) -> dict:
|
def restart_service(name: str) -> dict:
|
||||||
if name in SYSTEM_SERVICES:
|
if name in SYSTEM_SERVICES:
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
return err
|
return err
|
||||||
return _run(["sudo", "systemctl", "restart", name], sudo_password=sudo_password)
|
return _run(["sudo", "systemctl", "restart", name])
|
||||||
if name in USER_SERVICES:
|
if name in USER_SERVICES:
|
||||||
return _run(["systemctl", "--user", "restart", name])
|
return _run(["systemctl", "--user", "restart", name])
|
||||||
return {"ok": False, "err": f"Dienst '{name}' nicht erlaubt."}
|
return {"ok": False, "err": f"Dienst '{name}' nicht erlaubt."}
|
||||||
|
|
||||||
|
|
||||||
def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> dict:
|
def logs(service: str, lines: int = 200) -> dict:
|
||||||
lines = max(1, min(lines, 1000))
|
lines = max(1, min(lines, 1000))
|
||||||
if service in USER_SERVICES:
|
if service in USER_SERVICES:
|
||||||
r = _run(["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"])
|
r = _run(["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"])
|
||||||
@@ -609,15 +609,14 @@ def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> di
|
|||||||
r = _run(["journalctl", "-u", service, "-n", str(lines), "--no-pager"])
|
r = _run(["journalctl", "-u", service, "-n", str(lines), "--no-pager"])
|
||||||
if r["ok"]:
|
if r["ok"]:
|
||||||
return {"ok": True, "text": r["out"] or "(keine Log-Einträge)"}
|
return {"ok": True, "text": r["out"] or "(keine Log-Einträge)"}
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
return err
|
return err
|
||||||
r = _run(["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"],
|
r = _run(["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"])
|
||||||
sudo_password=sudo_password)
|
|
||||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||||
|
|
||||||
def check_updates_job(sudo_password: str | None = None) -> dict:
|
def check_updates_job() -> dict:
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def on_done():
|
def on_done():
|
||||||
@@ -625,7 +624,7 @@ def check_updates_job(sudo_password: str | None = None) -> dict:
|
|||||||
_comp_cache.update(ts=0.0, data=[]) # Hermes-Status ebenfalls neu berechnen lassen
|
_comp_cache.update(ts=0.0, data=[]) # Hermes-Status ebenfalls neu berechnen lassen
|
||||||
|
|
||||||
cmd = "sudo apt-get update"
|
cmd = "sudo apt-get update"
|
||||||
job_id = jobengine.start_job(["bash", "-c", cmd], "Nach Updates suchen", on_done=on_done, sudo_password=sudo_password)
|
job_id = jobengine.start_job(["bash", "-c", cmd], "Nach Updates suchen", on_done=on_done)
|
||||||
return {"ok": True, "job_id": job_id}
|
return {"ok": True, "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
@@ -637,10 +636,10 @@ def _maintenance_busy() -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def os_update_job(sudo_password: str | None = None) -> dict:
|
def os_update_job() -> dict:
|
||||||
if busy := _maintenance_busy():
|
if busy := _maintenance_busy():
|
||||||
return busy
|
return busy
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
return err
|
return err
|
||||||
# Nach dem apt-Upgrade den Stack funktional prüfen (Job wird rot, wenn etwas kaputt ging).
|
# Nach dem apt-Upgrade den Stack funktional prüfen (Job wird rot, wenn etwas kaputt ging).
|
||||||
# DEBIAN_FRONTEND wird INNERHALB von `sudo bash -c` gesetzt (nicht als `sudo VAR=… cmd`) —
|
# DEBIAN_FRONTEND wird INNERHALB von `sudo bash -c` gesetzt (nicht als `sudo VAR=… cmd`) —
|
||||||
@@ -649,11 +648,11 @@ def os_update_job(sudo_password: str | None = None) -> dict:
|
|||||||
"sudo bash -c 'DEBIAN_FRONTEND=noninteractive apt-get upgrade -y' "
|
"sudo bash -c 'DEBIAN_FRONTEND=noninteractive apt-get upgrade -y' "
|
||||||
f"&& bash {STACK_POSTCHECK}")
|
f"&& bash {STACK_POSTCHECK}")
|
||||||
job_id = jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)",
|
job_id = jobengine.start_job(["bash", "-c", cmd], "OS-Update (apt)",
|
||||||
group="maintenance", sudo_password=sudo_password)
|
group="maintenance")
|
||||||
return {"ok": True, "job_id": job_id}
|
return {"ok": True, "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
def engine_update_job(sudo_password: str | None = None) -> dict | None:
|
def engine_update_job() -> dict | None:
|
||||||
if not ENGINE_UPDATE_CMD:
|
if not ENGINE_UPDATE_CMD:
|
||||||
return None
|
return None
|
||||||
if busy := _maintenance_busy():
|
if busy := _maintenance_busy():
|
||||||
@@ -674,7 +673,7 @@ def engine_update_job(sudo_password: str | None = None) -> dict | None:
|
|||||||
return {"ok": True, "job_id": job_id}
|
return {"ok": True, "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
def swap_update_job(sudo_password: str | None = None) -> dict | None:
|
def swap_update_job() -> dict | None:
|
||||||
if not SWAP_UPDATE_CMD:
|
if not SWAP_UPDATE_CMD:
|
||||||
return None
|
return None
|
||||||
if busy := _maintenance_busy():
|
if busy := _maintenance_busy():
|
||||||
@@ -746,12 +745,12 @@ def hermes_update_job() -> dict:
|
|||||||
return {"ok": True, "job_id": job_id}
|
return {"ok": True, "job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
def update_all_job(sudo_password: str | None = None) -> dict:
|
def update_all_job() -> dict:
|
||||||
"""„Alle aktualisieren": kettet die AUSSTEHENDEN Updates sequenziell in EINEM Job —
|
"""„Alle aktualisieren": kettet die AUSSTEHENDEN Updates sequenziell in EINEM Job —
|
||||||
Engine → Router → Hermes → OS. Bewusst nur Wiederverwendung: jeder Teil ist exakt der
|
Engine → Router → Hermes → OS. Bewusst nur Wiederverwendung: jeder Teil ist exakt der
|
||||||
Befehl des Einzel-Updates (mit eigenem Backup/Postcheck/Rollback). `&&`-Kette = bei
|
Befehl des Einzel-Updates (mit eigenem Backup/Postcheck/Rollback). `&&`-Kette = bei
|
||||||
Fehler stoppt der Rest (das Log zeigt, wo). OS zuletzt, weil apt am breitesten eingreift;
|
Fehler stoppt der Rest (das Log zeigt, wo). OS zuletzt, weil apt am breitesten eingreift;
|
||||||
es braucht als einziges das Box-Passwort — fehlt es, laufen die sudo-freien Teile trotzdem."""
|
es ist das einzige mit sudo — scheitert das, laufen die sudo-freien Teile trotzdem."""
|
||||||
if busy := _maintenance_busy():
|
if busy := _maintenance_busy():
|
||||||
return busy
|
return busy
|
||||||
upd = updates()
|
upd = updates()
|
||||||
@@ -764,9 +763,10 @@ def update_all_job(sudo_password: str | None = None) -> dict:
|
|||||||
parts.append(("Hermes-Agent", _hermes_update_cmd()))
|
parts.append(("Hermes-Agent", _hermes_update_cmd()))
|
||||||
os_pending = (upd.get("os") or 0) > 0
|
os_pending = (upd.get("os") or 0) > 0
|
||||||
if os_pending:
|
if os_pending:
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
# Ohne Passwort: OS auslassen statt alles zu blockieren — aber nur, wenn
|
# sudo verlangt wider Erwarten ein Passwort (sudoers geaendert?): OS auslassen
|
||||||
# es überhaupt sudo-freie Teile gibt; sonst ehrlich das Passwort verlangen.
|
# statt alles zu blockieren — aber nur, wenn es sudo-freie Teile gibt; sonst
|
||||||
|
# den Fehler ehrlich durchreichen.
|
||||||
if not parts:
|
if not parts:
|
||||||
return err
|
return err
|
||||||
os_pending = False
|
os_pending = False
|
||||||
@@ -791,12 +791,11 @@ def update_all_job(sudo_password: str | None = None) -> dict:
|
|||||||
|
|
||||||
labels = " → ".join(label for label, _ in parts)
|
labels = " → ".join(label for label, _ in parts)
|
||||||
job_id = jobengine.start_job(["bash", "-c", cmd], f"Alle aktualisieren ({labels})",
|
job_id = jobengine.start_job(["bash", "-c", cmd], f"Alle aktualisieren ({labels})",
|
||||||
group="maintenance", on_done=on_done,
|
group="maintenance", on_done=on_done)
|
||||||
sudo_password=sudo_password)
|
|
||||||
return {"ok": True, "job_id": job_id, "parts": [label for label, _ in parts]}
|
return {"ok": True, "job_id": job_id, "parts": [label for label, _ in parts]}
|
||||||
|
|
||||||
|
|
||||||
def reboot(sudo_password: str | None = None) -> dict:
|
def reboot() -> dict:
|
||||||
if err := check_sudo_needs_password(sudo_password):
|
if err := check_sudo_needs_password():
|
||||||
return err
|
return err
|
||||||
return _run(["sudo", "reboot"], sudo_password=sudo_password)
|
return _run(["sudo", "reboot"])
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Lane-Routing für den eingebauten MC2-Gateway (:9001/v1).
|
|||||||
|
|
||||||
Zwei virtuelle Lanes, die Clients/IDEs auswählen — der Router pickt das echte Modell:
|
Zwei virtuelle Lanes, die Clients/IDEs auswählen — der Router pickt das echte Modell:
|
||||||
- **chat** (= altes `auto`): Alltag → `fast`, schwer/lang → `heavy`.
|
- **chat** (= altes `auto`): Alltag → `fast`, schwer/lang → `heavy`.
|
||||||
- **coding**: Code-Arbeit → `coder` (Qwen3-Coder-Next); riesiger/architektonischer Kontext → `heavy`;
|
- **coding**: Code-Arbeit → `coder`; riesiger/architektonischer Kontext → `heavy`;
|
||||||
triviale Kurzfrage ohne Code → `fast` (Tempo).
|
triviale Kurzfrage ohne Code → `fast` (Tempo).
|
||||||
|
|
||||||
Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases liegen in einer UI-editierbaren Policy
|
Regelbasiert, sub-ms, ohne Cloud. Schwellen/Aliases liegen in einer UI-editierbaren Policy
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Health-Wächter der Box (Lucy-Proaktivität, Faden A3).
|
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, MEM0_SERVICE_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"
|
||||||
+37
-2
@@ -1,6 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# ‼️ 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() {
|
||||||
|
|
||||||
echo "Starte MC2 Deployment..."
|
echo "Starte MC2 Deployment..."
|
||||||
|
|
||||||
# 1. Neuesten Code holen
|
# 1. Neuesten Code holen
|
||||||
@@ -12,11 +21,30 @@ 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
|
||||||
|
#
|
||||||
|
# ‼️ ACHTUNG (27.08.2026): /etc/llama-swap/config.yaml ist die LEBENDE Wahrheit — MC2
|
||||||
|
# schreibt sie selbst (backend/services/llamaswap.py::write_config), sobald jemand über
|
||||||
|
# die Oberfläche ein Modell hinzufügt oder eine Rolle ändert. deploy/llama-swap.config.yaml
|
||||||
|
# ist nur ein Abzug und hinkt regelmäßig hinterher (erlebt: der Coder-Vollkontext aus 34a9862
|
||||||
|
# stand tagelang nur live, nicht im Abzug — ein Deploy hätte ihn zurückgesetzt).
|
||||||
|
# Bis hierher wurde die Live-Datei kommentarlos mit dem Abzug überschrieben — alles per
|
||||||
|
# UI Hinzugefügte war nach jedem Deploy weg. Jetzt: erst sichern, dann kopieren, und die
|
||||||
|
# Abweichung sichtbar machen. MC_DEPLOY_SKIP_SWAP_CONFIG=1 überspringt den Schritt ganz.
|
||||||
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||||
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
|
if [ "${MC_DEPLOY_SKIP_SWAP_CONFIG:-0}" = "1" ]; then
|
||||||
|
echo "Llama-Swap Konfiguration weicht ab — uebersprungen (MC_DEPLOY_SKIP_SWAP_CONFIG=1)."
|
||||||
|
else
|
||||||
|
SWAP_BAK="/etc/llama-swap/config.yaml.bak-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
echo "Llama-Swap Konfiguration hat sich geändert."
|
||||||
|
echo " Abweichung (live -> Repo-Abzug):"
|
||||||
|
diff -u /etc/llama-swap/config.yaml deploy/llama-swap.config.yaml | head -40 || true
|
||||||
|
echo " Sichere die laufende Datei nach $SWAP_BAK"
|
||||||
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
# 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 cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||||
sudo systemctl restart llama-swap
|
sudo systemctl restart llama-swap
|
||||||
|
echo " Rueckweg bei Verlust: sudo cp $SWAP_BAK /etc/llama-swap/config.yaml"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||||
@@ -35,7 +63,14 @@ for skill_dir in deploy/skills/*; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
# 5. Dienste neu starten
|
# 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..."
|
echo "Starte Backend neu..."
|
||||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
systemctl --user restart mc2-gateway.service mission-control-2.service mc2-steward.service
|
||||||
|
|
||||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
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 ---
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# Die drei Jobs (KISS-Umbau, 21.08.2026)
|
||||||
|
|
||||||
|
Vorher: **4 Hermes-Crons + 6 systemd-Timer**, verteilt auf zwei Mechanismen.
|
||||||
|
Deshalb fiel am 20.08. tagelang niemandem auf, dass ein Waechter fehlte —
|
||||||
|
niemand schaut an zwei Orten nach.
|
||||||
|
|
||||||
|
Jetzt: **drei Jobs, ein Ort.** `hermes cron list` zeigt die gesamte Automatik.
|
||||||
|
|
||||||
|
| Job | Wann | Art | Skript |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Daily News Report** | taeglich 07:00 | Agent + Websuche | — |
|
||||||
|
| **KI und Stack Radar** | samstags 08:00 | `--no-agent` | `stack-radar.sh` → `stack-ist.sh` |
|
||||||
|
| **Updates am Sonntag** | sonntags 04:30 | `--no-agent` | `sonntags-update.sh` → `autoupdate.sh` |
|
||||||
|
|
||||||
|
Daneben laufen als stille Rohrleitung weiter: `mc2-backup` (03:33) und
|
||||||
|
`projekte-sync` (stuendlich). Die melden sich nie, die sichern und synchronisieren nur.
|
||||||
|
|
||||||
|
## Der Entwurfsgrundsatz
|
||||||
|
|
||||||
|
> **Fakten sammelt ein Skript, Prosa schreibt das Modell.**
|
||||||
|
|
||||||
|
Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren: das
|
||||||
|
Kritiker-Gate zeigte seit einem Tag auf ein umbenanntes Modell (404), Lucys
|
||||||
|
`SOUL.md` war blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
||||||
|
Er hatte geprueft, ob Dienste **antworten** — nicht, ob sie **stimmen**.
|
||||||
|
|
||||||
|
`stack-ist.sh` prueft deshalb Ergebnisse: loesen die Rollen-Aliase noch auf? Ist
|
||||||
|
der letzte Timer-Lauf gutgegangen? Ist eine Sicherung juenger als zwei Tage?
|
||||||
|
Liefert die oeffentliche Adresse Daten ohne Anmeldung? Beim ersten Lauf hat es
|
||||||
|
sofort zwei echte Befunde gefunden.
|
||||||
|
|
||||||
|
## Kugelsicher heisst konkret
|
||||||
|
|
||||||
|
- **Das Modell steht nicht im kritischen Pfad.** Antwortet es nicht, gehen die
|
||||||
|
Rohbefunde trotzdem raus. Nur die Prosa faellt weg, nie die Meldung.
|
||||||
|
- **Es meldet immer.** „Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
||||||
|
- **Kein `set -e`.** Ein einzelner fehlschlagender Test darf den Bericht nicht
|
||||||
|
abschneiden.
|
||||||
|
- **Ein Melderweg:** `deploy/notify.sh` erreicht Telegram **und** Lucys
|
||||||
|
Briefkasten (aus dem sie spricht) in einem Aufruf. Die Jobs laufen mit
|
||||||
|
`--deliver local`, damit Hermes nicht ein zweites Mal sendet.
|
||||||
|
- **Kein Fuellmaterial.** Die Prompts verbieten ausgedachte Vorschlaege
|
||||||
|
ausdruecklich — ein Bericht mit Fuellmaterial wird nicht gelesen, und dann
|
||||||
|
auch der echte Befund nicht.
|
||||||
|
|
||||||
|
## Ausbringen
|
||||||
|
|
||||||
|
Die Skripte muessen unter `~/.hermes/scripts/` liegen (Vorgabe von `hermes cron --script`).
|
||||||
|
Quelle ist dieses Verzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp deploy/jobs/*.sh hitonabi@192.168.178.151:/home/hitonabi/.hermes/scripts/
|
||||||
|
ssh hitonabi@192.168.178.151 'cd ~/.hermes/scripts && sed -i "s/\r$//" *.sh && chmod +x *.sh'
|
||||||
|
```
|
||||||
|
|
||||||
|
‼️ Das `sed` ist Pflicht: vom Windows-PC kopierte Dateien haben CRLF, und bash
|
||||||
|
scheitert daran mit unverstaendlichen Meldungen.
|
||||||
|
|
||||||
|
## Abgeschaltet am 21.08.
|
||||||
|
|
||||||
|
| Weg | Warum |
|
||||||
|
|---|---|
|
||||||
|
| Cron `morgen-digest` | geht im Daily News Report auf |
|
||||||
|
| Cron `tech-radar` | geht im Stack Radar auf |
|
||||||
|
| Cron `nacht-wartung` | pruefte Lebenszeichen, nicht Ergebnisse |
|
||||||
|
| Cron `wissens-sync` | **war nie gelaufen** — kein `last_run_at` |
|
||||||
|
| Timer `mc2-bagatell` | **15 Naechte hintereinander „0 eingespielt"** |
|
||||||
|
| Timer `mc2-selfsmoke` | geht in `stack-ist.sh` auf |
|
||||||
|
| Timer `pbs-backup` | doppelt zu `mc2-backup` und seit 21.08. rot (`/srv/models/mem0` gibt es nicht mehr) |
|
||||||
|
| Timer `mc2-autoupdate` | wird jetzt vom Cron „Updates am Sonntag" gestartet |
|
||||||
|
|
||||||
|
Die Unit-Dateien liegen noch da, nur `disable`d — Rueckbau ist ein Befehl.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nachtrag 21.08.: Persona, Quellen-Links und Stimme
|
||||||
|
|
||||||
|
**Fehler, den ich gemacht hatte:** Mein erster Prompt fuer den Daily News Report
|
||||||
|
schrieb woertlich *„keine Emojis, keine Aufzaehlungspunkte, keine
|
||||||
|
Ueberschriften-Deko"* — und hat damit genau das wegoptimiert, was den Bericht
|
||||||
|
vorher gut machte.
|
||||||
|
|
||||||
|
★★ **Die Formatierung kam nie aus dem Job-Prompt.** Die alten Prompts waren
|
||||||
|
kurz (*„eine praegnante 3-Punkte-Zusammenfassung an den Commander"*). Emojis,
|
||||||
|
Ton und die Anrede stehen in **`~/.hermes/SOUL.md`** — Lucys Persona:
|
||||||
|
|
||||||
|
> Du sprichst den Nutzer IMMER mit **Commander** an.
|
||||||
|
> Locker, herzlich, schlagfertig, charmant, selbstbewusst.
|
||||||
|
|
||||||
|
**Lehre: den Job-Prompt kurz halten und die Persona arbeiten lassen.** Wer im
|
||||||
|
Prompt Stil verbietet, ueberstimmt die Persona — und merkt es erst, wenn die
|
||||||
|
Nachricht seelenlos ankommt.
|
||||||
|
|
||||||
|
### Stimme in Telegram
|
||||||
|
|
||||||
|
`hermes send` kann **`MEDIA:<pfad>`**. Damit geht eine fertige Audiodatei als
|
||||||
|
Anhang nach Telegram. `deploy/jobs/news-melden.sh` macht daraus einen Aufruf:
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent schreibt zwei Dateien
|
||||||
|
/tmp/news-text.md (Emojis, Links, Formatierung) -> notify.sh -> Telegram + Briefkasten
|
||||||
|
/tmp/news-sprich.txt (3-4 Saetze, nichts Vorlesbares fehlt) -> :8650/tts -> WAV -> hermes send MEDIA:
|
||||||
|
```
|
||||||
|
|
||||||
|
‼️ **Nicht Hermes' eingebautes TTS nehmen.** Das steht auf `tts.provider: edge`
|
||||||
|
mit `en-US-AriaNeural` — englisch. Lucys echte Stimme ist MC2s `voice-service`
|
||||||
|
auf `:8650` (Piper `de_DE-thorsten-medium`). Deshalb ruft das Skript den
|
||||||
|
Sidecar direkt per curl.
|
||||||
|
|
||||||
|
‼️ **Kein ffmpeg noetig.** Telegram nimmt die WAV direkt an. Fuer eine echte
|
||||||
|
Sprachnachricht mit Wellenform braeuchte es OGG/Opus und damit ffmpeg — bewusst
|
||||||
|
nicht installiert, eine Abhaengigkeit weniger.
|
||||||
|
|
||||||
|
Der Agent ruft **einen** Befehl auf, alles danach ist deterministisch. Faellt
|
||||||
|
die Stimme aus, geht der Text trotzdem raus.
|
||||||
|
|
||||||
|
### ‼️ Folgefehler der Werkzeug-Abschaltung — gefunden und behoben
|
||||||
|
|
||||||
|
Nach dem Abschalten von `kanban` und `delegation` standen in `SOUL.md` noch
|
||||||
|
zwei Anweisungen, die ins Leere zeigten: *„rufst du sofort dein Werkzeug
|
||||||
|
delegate_task auf"* und *„Ideen traegst du sofort im Kanban-Auftragsbuch ein"*.
|
||||||
|
Genau die Klasse stiller Defekt, die am 19.08. das Kritiker-Gate zerlegt hat.
|
||||||
|
Beide Abschnitte ersetzt (Sicherung: `~/.hermes/SOUL.md.bak-20260821`).
|
||||||
|
|
||||||
|
**Merke: wer einen Werkzeugsatz abschaltet, muss `SOUL.md` mitlesen.**
|
||||||
|
|
||||||
|
### Kugelsicher-Regeln, die dazugekommen sind
|
||||||
|
|
||||||
|
- `hermes cron edit <id> "text"` **speichert nichts** — der Prompt muss ueber
|
||||||
|
**`--prompt`** kommen. Ohne Flag gibt der Befehl den Text nur aus und die
|
||||||
|
alte Fassung bleibt stehen. Nach jeder Aenderung in `jobs.json` nachsehen.
|
||||||
|
- Alte Job-Fassungen liegen im Zustands-Backup:
|
||||||
|
`tar -xzf /srv/models/mc2-backups/mc2-state-*.tar.gz ./hermes/cron/jobs.json`
|
||||||
|
- Tagesaktualitaet muss man erzwingen: Datum per `date` feststellen lassen,
|
||||||
|
mehrere Suchen verlangen, und Meldungen ohne belegbares Datum verwerfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nachtrag 3 (21.08.): Lucys ECHTE Stimme, echte Sprachnachricht
|
||||||
|
|
||||||
|
### Der Fehler: :8650 ist nicht Lucy
|
||||||
|
|
||||||
|
Ich hatte `/tts` auf MC2s `voice-service` (`:8650`) ohne Angabe von Engine und
|
||||||
|
Stimme aufgerufen. Dessen `/health` sagt:
|
||||||
|
|
||||||
|
```
|
||||||
|
"engines":["elevenlabs","edge"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Piper und Chatterbox sind dort **gar nicht geladen** — die Vorgabe fiel auf die
|
||||||
|
erste verfuegbare: ElevenLabs *„Artoria DE · Saber · Hermes-Stimme"*. Das ist
|
||||||
|
**Hermes' Stimme**, nicht Lucys. Deutsch und weiblich, deshalb faellt es nicht
|
||||||
|
sofort auf. Beide verfuegbaren Engines sind ausserdem **Cloud** — gegen die
|
||||||
|
100-%-lokal-Praemisse.
|
||||||
|
|
||||||
|
### Lucys Stimme: `lucy-stimme.service` auf `:8021`
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Engine | **Kyutai pocket-tts 2.1.0** (neueste, seit 04.05. unveraendert) |
|
||||||
|
| Modell | **`german_24l`** — die volle Fassung, nicht die destillierte |
|
||||||
|
| Stimme | geklont aus `ref.mp3`, gecacht in `lucy_voice.safetensors` (44 MB) |
|
||||||
|
| Laeuft | `~/.lucy-stimme/`, systemd-**user**-Dienst, `enable`d, CPU |
|
||||||
|
| Start | ~48 s Ladezeit, danach ~5 s fuer 3,7 s Audio |
|
||||||
|
|
||||||
|
Mitgezogen wurde die **ganze Abstimmung**, nicht nur das Modell: `text_norm.py`
|
||||||
|
(Symbole/Pfade/URLs → Zahlen → Akronyme), Emotions-Voreinstellungen mit
|
||||||
|
Anlaufwoertern, Umlaut-Wortliste, Hochpass auf der Referenz, kalibrierter Pegel.
|
||||||
|
Ohne die klingt pocket nicht wie Lucy.
|
||||||
|
|
||||||
|
‼️ Der Sweep im TTS-Plan (*„seriell schlaegt parallel"*) wurde auf einem **9700X**
|
||||||
|
gemessen. Die Box ist ein Ryzen AI MAX+ 395 — das Ergebnis ist **nicht
|
||||||
|
uebertragen**, nur uebernommen. Wer Tempo braucht, misst neu.
|
||||||
|
|
||||||
|
### Recherche 21.08.: pocket bleibt
|
||||||
|
|
||||||
|
Nichts seit Mai schlaegt es auf dieser Achse. Die Alternativen sind 5–17× groesser:
|
||||||
|
Qwen3-TTS 0,6–1,7 Mrd. (Apache 2.0, Deutsch, 3-Sekunden-Klon), NeuTTS Air 0,5 Mrd.
|
||||||
|
(GGUF), CosyVoice2 0,5 Mrd. — gegen pockets **100 Mio.** Fuer einen taeglichen
|
||||||
|
Sprachnachrichten-Job auf CPU ist das der falsche Handel.
|
||||||
|
**Ihr wart bereits auf dem neuesten Stand** — nichts zu aktualisieren.
|
||||||
|
|
||||||
|
### Echte Sprachnachricht braucht OGG/Opus
|
||||||
|
|
||||||
|
Eine WAV kommt in Telegram als **Dateianhang** an. Fuer das runde Sprachmemo mit
|
||||||
|
Wellenform braucht es OGG/Opus — dafuer wurde **ffmpeg installiert** (8.0.1):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffmpeg -y -i ton.wav -c:a libopus -b:a 32k -ar 48000 -ac 1 ton.ogg
|
||||||
|
hermes send --to telegram "MEDIA:ton.ogg"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ganze Kette (Text + Stimme + Versand) gemessen: **10 Sekunden**.
|
||||||
|
|
||||||
|
### Altlast am Rande
|
||||||
|
|
||||||
|
`F:\Coding Stuff\lucy\lucy-tts\Lucy-Startklar.bat` zeigt auf
|
||||||
|
`mission-control-2\client\lucy-tts` — den Ordner gibt es seit der Repo-Trennung
|
||||||
|
nicht mehr.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# news-melden.sh — schickt den fertigen Nachrichtenbericht als TEXT und als
|
||||||
|
# SPRACHNACHRICHT an den Commander (21.08.2026, überarbeitet 22.08.).
|
||||||
|
#
|
||||||
|
# Der Agent ruft genau EINEN Befehl auf. Alles danach ist deterministisch —
|
||||||
|
# Zustellung darf nicht davon abhaengen, ob ein Modell die Reihenfolge einhaelt.
|
||||||
|
#
|
||||||
|
# ‼️ WARUM DAS SKRIPT SOFORT ZURUECKKEHRT (Fund vom 22.08., erster echter Lauf):
|
||||||
|
# Hermes' terminal-Werkzeug bricht nach 30 s ab. Die Sprachsynthese dauert
|
||||||
|
# laenger. Der Agent sah den Timeout, hielt den Aufruf fuer gescheitert und
|
||||||
|
# rief das Skript ein zweites Mal auf — der Text ging DOPPELT raus
|
||||||
|
# (07:01:42 und 07:02:14).
|
||||||
|
# Deshalb: Text senden, Stimme abgekoppelt im Hintergrund, sofort raus.
|
||||||
|
# Dazu eine Doppelversand-Sperre, die auch kuenftige Wiederholungen abfaengt.
|
||||||
|
#
|
||||||
|
# Erwartet zwei Dateien, die der Agent vorher geschrieben hat:
|
||||||
|
# $1 Textfassung (mit Emojis, Links, Formatierung — geht nach Telegram)
|
||||||
|
# $2 Sprechfassung (kurz, ohne Links/Emojis — wird vorgelesen)
|
||||||
|
#
|
||||||
|
# ‼️ STIMME: :8021, nicht :8650.
|
||||||
|
# :8021 = lucy-stimme.service — Kyutai pocket-tts, Modell `german_24l`,
|
||||||
|
# Stimme geklont aus ref.mp3. DAS ist Lucy. Lokal, auf der CPU.
|
||||||
|
# :8650 = MC2s voice-service. Dort sind nur `elevenlabs` und `edge` geladen —
|
||||||
|
# beides CLOUD, und die Vorgabe ist die ElevenLabs-Stimme
|
||||||
|
# "Artoria/Saber", also HERMES' Stimme, nicht Lucys.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||||
|
VOICE="${VOICE_URL:-http://127.0.0.1:8021}"
|
||||||
|
HERMES="${HERMES_BIN:-$HOME/.local/bin/hermes}"
|
||||||
|
TON="/tmp/news-stimme.wav"
|
||||||
|
TON_OGG="/tmp/news-stimme.ogg"
|
||||||
|
|
||||||
|
# ---------------------------------------------- Zweiter Durchgang: Stimme ---
|
||||||
|
# Wird vom ersten Durchgang abgekoppelt neu gestartet. Laeuft beliebig lange,
|
||||||
|
# ohne dass irgendein Werkzeug-Timeout hineinredet.
|
||||||
|
if [ "${1:-}" = "--nur-stimme" ]; then
|
||||||
|
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
||||||
|
[ -s "$SPRECHDATEI" ] || exit 0
|
||||||
|
|
||||||
|
CODE=$(curl -s -m 600 -X POST "$VOICE/tts" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data "$(python3 -c 'import json,sys; print(json.dumps({"text": open(sys.argv[1], encoding="utf-8").read()[:1500]}))' "$SPRECHDATEI")" \
|
||||||
|
-o "$TON" -w '%{http_code}' 2>/dev/null)
|
||||||
|
|
||||||
|
[ "$CODE" = "200" ] && [ -s "$TON" ] || exit 0
|
||||||
|
|
||||||
|
# Telegram zeigt eine ECHTE Sprachnachricht (runde Blase, Wellenform) nur bei
|
||||||
|
# OGG/Opus. Eine WAV kaeme als Dateianhang an. Mono/48k ist das erwartete Format.
|
||||||
|
if ! ffmpeg -y -loglevel error -i "$TON" -c:a libopus -b:a 32k -ar 48000 -ac 1 "$TON_OGG" 2>/dev/null; then
|
||||||
|
TON_OGG="$TON"
|
||||||
|
fi
|
||||||
|
"$HERMES" send --to telegram "MEDIA:$TON_OGG" >/dev/null 2>&1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------- Erster Durchgang: Text + Start ---
|
||||||
|
TEXTDATEI="${1:-/tmp/news-text.md}"
|
||||||
|
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
||||||
|
|
||||||
|
if [ ! -s "$TEXTDATEI" ]; then
|
||||||
|
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Doppelversand-Sperre. Greift, egal WARUM ein zweiter Aufruf kommt —
|
||||||
|
# Timeout, Modell-Wiederholung, versehentlicher Handstart.
|
||||||
|
MARKE="/tmp/.news-gesendet-$(sha256sum "$TEXTDATEI" | cut -c1-16)"
|
||||||
|
if [ -f "$MARKE" ]; then
|
||||||
|
ALTER=$(( $(date +%s) - $(stat -c %Y "$MARKE" 2>/dev/null || echo 0) ))
|
||||||
|
if [ "$ALTER" -lt 7200 ]; then
|
||||||
|
echo "gemeldet (dieser Bericht ging vor ${ALTER}s bereits raus — nicht doppelt geschickt)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
: > "$MARKE"
|
||||||
|
find /tmp -maxdepth 1 -name '.news-gesendet-*' -mtime +2 -delete 2>/dev/null
|
||||||
|
|
||||||
|
bash "$NOTIFY" -s "" "$(cat "$TEXTDATEI")"
|
||||||
|
|
||||||
|
# Stimme abkoppeln: eigene Sitzung, kein Elternprozess, keine offenen Deskriptoren.
|
||||||
|
# Damit ueberlebt sie das Ende dieses Skripts und blockiert es nicht.
|
||||||
|
if [ -s "$SPRECHDATEI" ]; then
|
||||||
|
setsid nohup bash "$0" --nur-stimme "$SPRECHDATEI" >/dev/null 2>&1 < /dev/null &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "gemeldet"
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sonntags-update.sh — Job 2: "Updates am Sonntag" (21.08.2026).
|
||||||
|
#
|
||||||
|
# Duenner Aufsatz auf deploy/autoupdate.sh. Bewusst KEIN Neubau: autoupdate.sh
|
||||||
|
# macht seit Juli genau das Richtige — Router -> Engine -> Hermes, jede Ebene
|
||||||
|
# mit Update-Pruefung, Postcheck, und bei Rot automatischem Rueckbau plus
|
||||||
|
# Selbst-Pinnung. Es meldet auch schon selbst ueber notify.sh, also Telegram
|
||||||
|
# UND Lucys Stimme.
|
||||||
|
#
|
||||||
|
# Dieser Aufsatz existiert nur, damit der Job unter `hermes cron list` steht
|
||||||
|
# statt in einem separaten systemd-Timer. Ein Ort fuer alle Automatik — genau
|
||||||
|
# deshalb ist am 20.08. tagelang niemandem aufgefallen, dass ein Waechter fehlte.
|
||||||
|
#
|
||||||
|
# Aufruf ueber Hermes-Cron:
|
||||||
|
# hermes cron create '30 4 * * 0' --name 'Updates am Sonntag' \
|
||||||
|
# --no-agent --script sonntags-update.sh --deliver local
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
AUTOUPDATE="${AUTOUPDATE:-$HOME/mission-control-v2/deploy/autoupdate.sh}"
|
||||||
|
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||||
|
|
||||||
|
melde() { bash "$NOTIFY" -s "[Box-Update]" "$1" 2>/dev/null || echo "$1" >&2; }
|
||||||
|
|
||||||
|
if [ ! -f "$AUTOUPDATE" ]; then
|
||||||
|
melde "Sonntags-Update ausgefallen: $AUTOUPDATE gibt es nicht. Nichts wurde aktualisiert."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# autoupdate.sh meldet waehrend des Laufs selbst (inkl. der KRITISCH-Faelle, in
|
||||||
|
# denen ein Bericht am Ende zu spaet kaeme). Wir fangen hier nur den Fall ab,
|
||||||
|
# dass es gar nicht erst durchlaeuft — sonst waere das Schweigen zweideutig.
|
||||||
|
AUSGABE="$(bash "$AUTOUPDATE" 2>&1)"
|
||||||
|
CODE=$?
|
||||||
|
|
||||||
|
if [ "$CODE" -ne 0 ]; then
|
||||||
|
melde "Sonntags-Update mit Fehler beendet (exit $CODE). Letzte Zeilen:
|
||||||
|
|
||||||
|
$(printf '%s\n' "$AUSGABE" | tail -15)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Erfolgsfall: autoupdate.sh hat bereits gemeldet. Leere Ausgabe -> Hermes
|
||||||
|
# schweigt, also genau eine Nachricht statt zwei.
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# stack-ist.sh — der IST-Zustand der Box, in Fakten (21.08.2026).
|
||||||
|
#
|
||||||
|
# Wird von Hermes-Cron "stack-radar" ueber --script eingespeist: die Ausgabe hier
|
||||||
|
# landet im Prompt des Agenten, der daraus den Bericht schreibt.
|
||||||
|
#
|
||||||
|
# GRUNDSATZ: Dieses Skript prueft ERGEBNISSE, nicht Lebenszeichen.
|
||||||
|
# Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren:
|
||||||
|
# das Kritiker-Gate zeigte auf ein umbenanntes Modell (404), Lucys SOUL.md war
|
||||||
|
# blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
||||||
|
# "Dienst antwortet" haette das nie gefunden. Deshalb wird hier nachgesehen,
|
||||||
|
# ob Dinge STIMMEN — nicht ob sie laufen.
|
||||||
|
#
|
||||||
|
# Nie mit set -e: ein einzelner fehlschlagender Test darf den Bericht nicht
|
||||||
|
# abschneiden. Fehler werden gemeldet, nicht verschwiegen.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
MC2="${MC2_URL:-http://127.0.0.1:9001}"
|
||||||
|
SWAP="${SWAP_URL:-http://127.0.0.1:8080}"
|
||||||
|
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
||||||
|
OEFFENTLICH="${OEFFENTLICH_URL:-https://mc.tobisniceshomelab.ddnsfree.com}"
|
||||||
|
|
||||||
|
ok() { printf ' OK %s\n' "$*"; }
|
||||||
|
warn() { printf ' ACHTUNG %s\n' "$*"; }
|
||||||
|
bad() { printf ' ROT %s\n' "$*"; }
|
||||||
|
|
||||||
|
echo "IST-ZUSTAND DER BOX — $(date '+%d.%m.%Y %H:%M')"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Dienste ---
|
||||||
|
echo "DIENSTE"
|
||||||
|
# systemd stellt gescheiterten Units ein "●" voran — das muss weg, sonst meldet
|
||||||
|
# der Bericht lauter Aufzaehlungspunkte als kaputte Dienste.
|
||||||
|
# EGAL: Units, die dauerhaft scheitern und niemanden stoeren. Ein Bericht, der
|
||||||
|
# jeden Tag denselben harmlosen Fehler zeigt, erzieht zum Wegsehen.
|
||||||
|
EGAL="at-spi-dbus-bus.service"
|
||||||
|
fehl=$(systemctl --user list-units --type=service --state=failed --no-legend --no-pager 2>/dev/null \
|
||||||
|
| sed 's/^[^a-zA-Z]*//' | awk '{print $1}')
|
||||||
|
gefunden=0
|
||||||
|
for d in $fehl; do
|
||||||
|
case " $EGAL " in *" $d "*) continue ;; esac
|
||||||
|
bad "Dienst gescheitert: $d"; gefunden=1
|
||||||
|
done
|
||||||
|
[ "$gefunden" = "0" ] && ok "kein Dienst im Fehlerzustand"
|
||||||
|
|
||||||
|
for d in mc2-gateway mission-control-2 hermes-gateway voice-service; do
|
||||||
|
systemctl --user is-active --quiet "$d.service" 2>/dev/null \
|
||||||
|
&& ok "$d laeuft" || bad "$d laeuft NICHT"
|
||||||
|
done
|
||||||
|
systemctl is-active --quiet llama-swap.service 2>/dev/null \
|
||||||
|
&& ok "llama-swap laeuft" || bad "llama-swap laeuft NICHT"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Timer ---
|
||||||
|
# Nicht "ist der Timer aktiv", sondern "ist der letzte Lauf gutgegangen".
|
||||||
|
echo "TIMER — letzter Lauf"
|
||||||
|
for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk '{print $NF}'); do
|
||||||
|
dienst="${t%.service}.service"
|
||||||
|
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
||||||
|
code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null)
|
||||||
|
wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3)
|
||||||
|
if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen"
|
||||||
|
elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann"
|
||||||
|
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --------------------------------------------------------- Modell-Aliase ---
|
||||||
|
# Der Fund vom 19./20.08.: Skripte und Configs nennen Modelle beim Eigennamen,
|
||||||
|
# das Modell wird umbenannt, und alles laeuft still in 404. Hier wird geprueft,
|
||||||
|
# ob jeder Alias, den irgendwer benutzt, heute noch antwortet.
|
||||||
|
echo "MODELL-ALIASE (Rollen muessen aufloesen)"
|
||||||
|
liste=$(curl -s -m 10 "$MC2/v1/models" 2>/dev/null \
|
||||||
|
| python3 -c 'import sys,json; print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null)
|
||||||
|
if [ -z "$liste" ]; then bad "Modell-Liste nicht abrufbar ueber $MC2/v1"
|
||||||
|
else
|
||||||
|
ok "verfuegbar: $liste"
|
||||||
|
for rolle in hermes fast heavy; do
|
||||||
|
case " $liste " in
|
||||||
|
*" $rolle "*) ok "Rolle '$rolle' loest auf" ;;
|
||||||
|
*) bad "Rolle '$rolle' FEHLT — Skripte, die sie nennen, laufen in 404" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Erreichbar ---
|
||||||
|
echo "ERREICHBARKEIT"
|
||||||
|
for paar in "MC2:$MC2/api/health" "llama-swap:$SWAP/v1/models" "Gitea:$GITEA/api/v1/version"; do
|
||||||
|
name="${paar%%:*}"; url="${paar#*:}"
|
||||||
|
code=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
||||||
|
[ "$code" = "200" ] && ok "$name antwortet (200)" || bad "$name antwortet mit '$code'"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------ Offen im Internet? --------
|
||||||
|
# Der Fund vom 21.08.: MC2 hing ueber den Nginx Proxy Manager ungeschuetzt im
|
||||||
|
# Netz. ufw sah nichts davon, weil der Proxy im LAN steht. Also von aussen
|
||||||
|
# nachsehen — und zwar auf einem Endpunkt, der ECHTE Daten liefert.
|
||||||
|
echo "OEFFENTLICHE ERREICHBARKEIT"
|
||||||
|
code=$(curl -s -m 15 -o /tmp/.ist-oeff -w '%{http_code}' "$OEFFENTLICH/api/system/status" 2>/dev/null)
|
||||||
|
if [ "$code" = "200" ] && head -c1 /tmp/.ist-oeff 2>/dev/null | grep -q '{'; then
|
||||||
|
bad "MC2 liefert Systemdaten OHNE Anmeldung ins Internet ($OEFFENTLICH)"
|
||||||
|
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||||||
|
ok "oeffentlicher Zugang verlangt Anmeldung ($code)"
|
||||||
|
elif [ "$code" = "000" ]; then
|
||||||
|
ok "von aussen nicht erreichbar"
|
||||||
|
else
|
||||||
|
warn "oeffentlicher Zugang antwortet mit '$code' — nachsehen"
|
||||||
|
fi
|
||||||
|
rm -f /tmp/.ist-oeff
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- Ports ---
|
||||||
|
echo "OFFENE PORTS (ufw)"
|
||||||
|
sudo -n ufw status 2>/dev/null | grep ALLOW | sed 's/^/ /' || warn "ufw-Status nicht lesbar"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --------------------------------------------------------------- CI-Ampel ---
|
||||||
|
echo "CI-AMPEL (Gitea)"
|
||||||
|
T=$(tr -d '[:space:]' < "$HOME/.config/gitea/create-token" 2>/dev/null)
|
||||||
|
if [ -z "$T" ]; then warn "kein Gitea-Token — CI nicht pruefbar"
|
||||||
|
else
|
||||||
|
curl -s -m 20 -H "Authorization: token $T" "$GITEA/api/v1/user/repos?limit=100" 2>/dev/null \
|
||||||
|
| python3 -c '
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
repos = json.load(sys.stdin)
|
||||||
|
except Exception as e:
|
||||||
|
print(" ACHTUNG Repo-Liste nicht lesbar:", e); sys.exit(0)
|
||||||
|
print(f" OK {len(repos)} Repos in Gitea")
|
||||||
|
' 2>/dev/null || warn "Repo-Liste nicht lesbar"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Sicherung ---
|
||||||
|
echo "SICHERUNGEN"
|
||||||
|
# Pfad aus backup.sh: DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||||
|
SICHER="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
|
||||||
|
neuestes=$(find "$SICHER" -maxdepth 2 -type f -newermt '-2 days' 2>/dev/null | head -1)
|
||||||
|
anzahl=$(find "$SICHER" -maxdepth 2 -type f 2>/dev/null | wc -l)
|
||||||
|
if [ -n "$neuestes" ]; then
|
||||||
|
ok "Sicherung juenger als 2 Tage vorhanden ($anzahl Staende in $SICHER)"
|
||||||
|
elif [ "$anzahl" -gt 0 ]; then
|
||||||
|
bad "juengste Sicherung ist AELTER als 2 Tage ($anzahl Staende in $SICHER)"
|
||||||
|
else
|
||||||
|
bad "gar keine Sicherung in $SICHER"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Platz ---
|
||||||
|
echo "PLATZ UND LAST"
|
||||||
|
# Nur eindeutige Dateisysteme: / und /srv liegen hier auf demselben Geraet,
|
||||||
|
# doppelt gemeldet las sich das wie ein Befund.
|
||||||
|
df -h --output=source,size,used,avail,pcent,target / /srv 2>/dev/null \
|
||||||
|
| awk '!gesehen[$1]++ {printf " %s\n", $0}'
|
||||||
|
echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- Warm/Kalt ---
|
||||||
|
# ---------------------------------------------------------------- Curator ---
|
||||||
|
# Hierher gefaltet am 22.08.2026 aus dem Repo `curator-staleness-check`.
|
||||||
|
# Das war ein eigener Wachhund mit eigenem systemd-Timer und eigener CI-Ampel —
|
||||||
|
# und er ueberwachte einen Cron-Job ueber eine FEST VERDRAHTETE ID
|
||||||
|
# (a47910e2ef05), den es laengst nicht mehr gibt. Er haette bei jedem Lauf
|
||||||
|
# Alarm geschlagen fuer etwas, das gar nicht existiert.
|
||||||
|
# Hier steht dieselbe Frage in vier Zeilen, ohne Repo, Timer und Ampel.
|
||||||
|
echo "CURATOR"
|
||||||
|
AN=$(grep -A3 '^curator:' "$HOME/.hermes/config.yaml" 2>/dev/null | grep -m1 'enabled:' | awk '{print $2}')
|
||||||
|
if [ "${AN:-false}" != "true" ]; then
|
||||||
|
ok "Curator ist abgeschaltet — nichts zu ueberwachen"
|
||||||
|
else
|
||||||
|
# %Ts liefert GANZE Sekunden. %T@ waere ein Float — bash rechnet damit nicht
|
||||||
|
# und bricht mit "invalid arithmetic operator" ab.
|
||||||
|
JUENGSTES=$(find "$HOME/.hermes/logs/curator" -type f -name '*.log' -printf '%Ts\n' 2>/dev/null | sort -rn | head -1)
|
||||||
|
if [ -z "$JUENGSTES" ]; then
|
||||||
|
bad "Curator ist AN, hat aber noch nie etwas protokolliert"
|
||||||
|
else
|
||||||
|
TAGE=$(( ( $(date +%s) - JUENGSTES ) / 86400 ))
|
||||||
|
if [ "$TAGE" -le 7 ]; then ok "Curator war vor $TAGE Tagen zuletzt aktiv"
|
||||||
|
elif [ "$TAGE" -le 21 ]; then warn "Curator seit $TAGE Tagen still — laeuft er noch?"
|
||||||
|
else bad "Curator seit $TAGE Tagen still, obwohl eingeschaltet"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "MODELLE IM SPEICHER"
|
||||||
|
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
||||||
|
| python3 -c '
|
||||||
|
import sys, json
|
||||||
|
try:
|
||||||
|
d = json.load(sys.stdin)
|
||||||
|
except Exception:
|
||||||
|
print(" ACHTUNG llama-swap-Status nicht lesbar"); sys.exit(0)
|
||||||
|
lauf = d.get("running", [])
|
||||||
|
if not lauf:
|
||||||
|
print(" OK kein Modell geladen (alles kalt)")
|
||||||
|
for m in lauf:
|
||||||
|
print(" OK " + str(m.get("model")) + " - " + str(m.get("state")))
|
||||||
|
'
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "ENDE IST-ZUSTAND"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# stack-radar.sh — Job 3: "KI & Stack Radar" (21.08.2026).
|
||||||
|
#
|
||||||
|
# Ablauf: IST-Zustand holen (Fakten) -> Modell bewerten lassen (Prosa)
|
||||||
|
# -> ueber notify.sh melden (Telegram + Lucys Stimme)
|
||||||
|
#
|
||||||
|
# KUGELSICHER heisst hier konkret:
|
||||||
|
# * Die Fakten kommen aus stack-ist.sh, nicht aus dem Modell. Faellt das Modell
|
||||||
|
# aus, werden die ROTEN Zeilen trotzdem gemeldet — roh statt gar nicht.
|
||||||
|
# * Es meldet IMMER. "Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
||||||
|
# * Kein set -e: ein einzelner Fehlschlag darf den Bericht nicht abschneiden.
|
||||||
|
#
|
||||||
|
# Aufruf ueber Hermes-Cron:
|
||||||
|
# hermes cron create '0 8 * * 6' --name 'KI & Stack Radar' \
|
||||||
|
# --no-agent --script stack-radar.sh --deliver local
|
||||||
|
# (--no-agent, weil dieses Skript selbst meldet. Leere Ausgabe = Hermes schweigt.)
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HIER="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
IST_SKRIPT="${IST_SKRIPT:-$HOME/.hermes/scripts/stack-ist.sh}"
|
||||||
|
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||||
|
GATEWAY="${GATEWAY_URL:-http://127.0.0.1:9010/v1}"
|
||||||
|
MODELL="${RADAR_MODELL:-fast}" # bewusst NICHT heavy: das laedt 68 GB und der
|
||||||
|
# Radar meldet danach seinen eigenen Speicherverbrauch als Befund
|
||||||
|
|
||||||
|
[ -x "$IST_SKRIPT" ] || IST_SKRIPT="$HIER/stack-ist.sh"
|
||||||
|
|
||||||
|
melde() {
|
||||||
|
if [ -x "$NOTIFY" ] || [ -f "$NOTIFY" ]; then
|
||||||
|
bash "$NOTIFY" -s "[Stack-Radar]" "$1"
|
||||||
|
else
|
||||||
|
# Letzte Rueckfallebene: wenigstens ins Log, damit nichts still verschwindet.
|
||||||
|
echo "[stack-radar] MELDEWEG FEHLT — Bericht folgt roh:" >&2
|
||||||
|
echo "$1" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- 1. Fakten ---
|
||||||
|
IST="$(bash "$IST_SKRIPT" 2>&1)"
|
||||||
|
if [ -z "$IST" ]; then
|
||||||
|
melde "Radar konnte den IST-Zustand nicht erheben — $IST_SKRIPT lieferte nichts. Das ist selbst ein Befund: bitte nachsehen."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROT="$(printf '%s\n' "$IST" | grep -E '^\s+(ROT|ACHTUNG)' || true)"
|
||||||
|
ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)"
|
||||||
|
|
||||||
|
# ------------------------------------------------------- 2. Bewertung ------
|
||||||
|
# Das Modell bekommt NUR die Fakten und darf nichts dazuerfinden. Es soll
|
||||||
|
# einordnen, nicht ermitteln.
|
||||||
|
PROMPT="Du bist der Stack-Radar einer selbstgehosteten KI-Box. Unten steht der
|
||||||
|
gemessene IST-Zustand. Schreibe einen kurzen Bericht auf Deutsch, per Du.
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Erfinde NICHTS. Nur was unten steht.
|
||||||
|
- Beginne mit einer Zeile Gesamturteil.
|
||||||
|
- Dann: was ist ROT oder ACHTUNG, und was waere jeweils der naechste Schritt.
|
||||||
|
- Verbesserungsvorschlaege NUR, wenn sich aus den Zahlen unten wirklich etwas
|
||||||
|
Konkretes ergibt. Gibt es nichts, lass den Abschnitt ersatzlos weg. Schreibe
|
||||||
|
lieber gar keinen Vorschlag als einen ausgedachten - ein Bericht mit
|
||||||
|
Fuellmaterial wird nicht gelesen.
|
||||||
|
- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG,
|
||||||
|
nicht unter Verbesserungen.
|
||||||
|
- Wenn alles gruen ist, sag das in einem Satz und hoere auf.
|
||||||
|
- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter.
|
||||||
|
|
||||||
|
IST-ZUSTAND:
|
||||||
|
$IST"
|
||||||
|
|
||||||
|
ANTWORT=""
|
||||||
|
if NUTZLAST="$(python3 -c '
|
||||||
|
import json, sys
|
||||||
|
print(json.dumps({
|
||||||
|
"model": sys.argv[1],
|
||||||
|
"messages": [{"role": "user", "content": sys.argv[2]}],
|
||||||
|
"max_tokens": 900,
|
||||||
|
"temperature": 0.3,
|
||||||
|
}))' "$MODELL" "$PROMPT" 2>/dev/null)"; then
|
||||||
|
ANTWORT="$(curl -s -m 300 -X POST "$GATEWAY/chat/completions" \
|
||||||
|
-H 'Content-Type: application/json' -d "$NUTZLAST" 2>/dev/null \
|
||||||
|
| python3 -c '
|
||||||
|
import json, sys
|
||||||
|
try:
|
||||||
|
print(json.load(sys.stdin)["choices"][0]["message"]["content"].strip())
|
||||||
|
except Exception:
|
||||||
|
pass' 2>/dev/null)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- 3. Melden ---
|
||||||
|
if [ -n "$ANTWORT" ]; then
|
||||||
|
melde "$ANTWORT"
|
||||||
|
else
|
||||||
|
# Modell stumm oder kaputt -> die Fakten gehen TROTZDEM raus. Genau dafuer
|
||||||
|
# steht das Modell nicht im kritischen Pfad.
|
||||||
|
if [ -n "$ROT" ]; then
|
||||||
|
melde "Radar: Bewertung durch das Modell hat nicht geklappt — hier die Rohbefunde:
|
||||||
|
|
||||||
|
$ROT"
|
||||||
|
else
|
||||||
|
melde "Radar: alles gruen (Bewertung durch das Modell hat nicht geklappt, die Messung schon)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Hermes bekommt eine LEERE Ausgabe -> keine zweite Nachricht. Gemeldet wurde
|
||||||
|
# bereits ueber notify.sh, und das erreicht Telegram UND Lucys Stimme.
|
||||||
|
exit 0
|
||||||
@@ -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
|
||||||
@@ -26,7 +26,7 @@ models:
|
|||||||
# 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.
|
||||||
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
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
aliases:
|
aliases:
|
||||||
- coder
|
- coder
|
||||||
@@ -34,7 +34,9 @@ models:
|
|||||||
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.
|
||||||
@@ -78,7 +80,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",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 32768,
|
||||||
|
"output": 8192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hermes": {
|
||||||
|
"name": "hermes - Erkunden (immer warm)",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 65536,
|
||||||
|
"output": 8192
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coder": {
|
||||||
|
"name": "coder - Qwen3.8-27B (dicht) - Standard fuers Bauen, agentisch am staerksten",
|
||||||
|
"tool_call": true,
|
||||||
|
"limit": {
|
||||||
|
"context": 131072,
|
||||||
|
"output": 16384
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"small_model": "aibox/hermes",
|
||||||
|
"instructions": [
|
||||||
|
"C:/Users/TobisPC/.config/opencode/ARBEITSANWEISUNG.md"
|
||||||
|
],
|
||||||
|
"autoupdate": false,
|
||||||
|
"agent": {
|
||||||
|
"plan": {
|
||||||
|
"model": "aibox/heavy",
|
||||||
|
"permission": {
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": "deny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"webfetch": "allow",
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"ssh *": "deny",
|
||||||
|
"scp *": "deny",
|
||||||
|
"sftp *": "deny",
|
||||||
|
"ssh arcane@192.168.178.162 *": "allow",
|
||||||
|
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
||||||
|
"scp *arcane@192.168.178.162*": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten - nur lesen, laeuft auf dem immer warmen Hirn",
|
||||||
|
"model": "aibox/hermes",
|
||||||
|
"permission": {
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": "deny"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code - meldet Befunde, aendert nichts",
|
||||||
|
"model": "aibox/heavy",
|
||||||
|
"permission": {
|
||||||
|
"edit": "deny",
|
||||||
|
"bash": "deny"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# OpenCode-Plugin: MC2-Governor
|
||||||
|
|
||||||
|
**Hier liegt die Quelle. Der Einsatzort ist eine Kopie.**
|
||||||
|
|
||||||
|
```
|
||||||
|
deploy/opencode-plugin/mc2-governor.ts <- versioniert, hier aendern
|
||||||
|
| kopieren
|
||||||
|
v
|
||||||
|
%USERPROFILE%\.config\opencode\plugin\ <- wird von OpenCode automatisch geladen
|
||||||
|
```
|
||||||
|
|
||||||
|
Bis zum 21.08.2026 existierte diese Datei **nur** im Konfigordner des PCs — 11 KB
|
||||||
|
verhaltensbestimmender Code ohne Sicherung und ohne Historie. Deshalb liegt sie jetzt hier.
|
||||||
|
|
||||||
|
## Ausbringen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
Wirkt sofort in der naechsten OpenCode-Sitzung; OpenChamber muss dafuer nicht neu starten,
|
||||||
|
eine neue Sitzung reicht.
|
||||||
|
|
||||||
|
## Was es tut
|
||||||
|
|
||||||
|
1. **Werkzeug-Zaun** (`tool.execute.before`) — blockt Handgriffe, die ein Agent nie
|
||||||
|
unbeaufsichtigt tun darf. Leitplanke gegen Entgleisen, **keine** Sandbox.
|
||||||
|
2. **Pruef-Tor + Reparaturschleife** (`session.idle`) — sagt der Agent „fertig", laeuft der
|
||||||
|
Verify-Befehl aus der Datei `VERIFY` im Repo-Wurzelverzeichnis. ROT geht als naechster
|
||||||
|
Auftrag zurueck an den Agenten, bis zu `MC2_LOOP_MAX_ROUNDS` mal.
|
||||||
|
‼️ **Schlaeft derzeit:** es gibt in keinem Projekt eine `VERIFY`-Datei. Ohne sie tut
|
||||||
|
diese Funktion nichts — schadet aber auch nicht.
|
||||||
|
3. **Savepoint statt Zusammenfassen** (`session.compacted`) — beim Komprimieren fallen
|
||||||
|
still die Regeln aus dem Kontext; stattdessen wird `SAVEPOINT.md` geschrieben.
|
||||||
|
4. **Stimme** — Ereignisse gehen an MC2 `/api/voice/announce`, Lucy spricht sie.
|
||||||
|
|
||||||
|
Der zugehoerige **Governor-Proxy auf `:8100`** (Token-Zaehler mit Hart-Deckel, Juli 2026)
|
||||||
|
**existiert nicht mehr** — kein Port, kein Ordner, keine Unit. Nachgemessen am 21.08.2026.
|
||||||
|
Dieses Plugin haengt nicht an ihm.
|
||||||
|
|
||||||
|
## Schalter (Umgebungsvariablen)
|
||||||
|
|
||||||
|
| Variable | Default | Wirkung |
|
||||||
|
|---|---|---|
|
||||||
|
| `MC2_BOX_URL` | `http://192.168.178.151:9001` | MC2-Basis fuer Meldungen |
|
||||||
|
| `MC2_FENCE_OFF` | — | `1` schaltet den **ganzen** Zaun ab (alles oder nichts) |
|
||||||
|
| `MC2_LOOP_AUTOFIX` | an | `0` schaltet die Reparaturschleife ab |
|
||||||
|
| `MC2_LOOP_MAX_ROUNDS` | `3` | Runden der Reparaturschleife |
|
||||||
|
| `MC2_LOOP_ANNOUNCE` | an | `0` macht Lucy stumm |
|
||||||
|
| `MC2_LOOP_SILENT` | — | `1` = Meldungen ohne Ton |
|
||||||
|
|
||||||
|
## Aenderung 21.08.2026: `git push` ist frei
|
||||||
|
|
||||||
|
Der Zaun blockte urspruenglich **jedes** `git push` („Veroeffentlichen ist Sache des
|
||||||
|
Menschen"). Das passt nicht mehr: die Bahn ist jetzt PC → Gitea → Box, und jede Aenderung
|
||||||
|
soll auch im git landen. Ein Agent, der committen, aber nicht pushen darf, laesst Arbeit
|
||||||
|
auf einem einzelnen Rechner liegen.
|
||||||
|
|
||||||
|
**Erzwungenes Pushen bleibt verboten** — `--force`, `--mirror`, `--delete`. Das
|
||||||
|
ueberschreibt fremde Arbeit auf dem Server und ist genau die Klasse Unfall, gegen die der
|
||||||
|
Zaun existiert. `--force-with-lease` ist bewusst ausgenommen.
|
||||||
|
|
||||||
|
Beides gemessen: `git push --dry-run origin main` laeuft durch,
|
||||||
|
`git push --force --dry-run origin main` wird mit `[MC2-ZAUN]` abgewiesen.
|
||||||
|
|
||||||
|
## ‼️ Falle: das `bash`-Werkzeug ist auf Windows kein bash
|
||||||
|
|
||||||
|
Es laeuft in **PowerShell**. `tail`, `head`, `grep` und Freunde gibt es dort nicht, und
|
||||||
|
Pipes verhalten sich anders. Wer dem Agenten Befehle vorgibt, muss das beruecksichtigen.
|
||||||
|
|
||||||
|
## ‼️ Falle: Gitea-Anmeldung ist flatterhaft
|
||||||
|
|
||||||
|
Der Git Credential Manager haelt fuer die DDNS-Domain ein **OAuth-Token mit einer Stunde
|
||||||
|
Laufzeit**. Laeuft es ab, scheitert der Push des Agenten mit
|
||||||
|
`remote: Failed to authenticate user` — ein Push aus PowerShell erneuert es, danach geht es
|
||||||
|
wieder. Dauerhafte Loesung waere ein langlebiges Gitea-Token auf die **interne** Adresse
|
||||||
|
`http://192.168.178.153:3000` (die DDNS-Domain ist nachts durch die Zwangstrennung ohnehin
|
||||||
|
zeitweise tot).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ‼️ ABGESCHALTET am 22.08.2026
|
||||||
|
|
||||||
|
**Das Plugin ist nicht mehr aktiv.** Es liegt hier als Quelle und im Konfigordner
|
||||||
|
unter `plugin-archiv/`, aber nicht mehr in `~/.config/opencode/plugin/`.
|
||||||
|
|
||||||
|
**Warum:** Beim ersten echten Arbeitslauf fiel auf, dass bei jeder Komprimierung
|
||||||
|
zwei Dinge uebereinander passierten — OpenChamber dampfte den Verlauf ein, und der
|
||||||
|
Governor schob sofort einen zusaetzlichen Auftrag nach („schreib SAVEPOINT.md").
|
||||||
|
Der Agent verlor dadurch jedes Mal einen Zug an Buchhaltung statt an die Aufgabe.
|
||||||
|
Nachgewiesen: `SAVEPOINT.md` in `mc2-referenz`, geschrieben 22.08. 14:47 mitten
|
||||||
|
im Lauf.
|
||||||
|
|
||||||
|
Der User hatte den Abbau schon am 21.08. angeordnet („alle Guards, alle
|
||||||
|
Governor, Stewards — brauchen wir nicht mehr"). Ich hatte damals das Behalten
|
||||||
|
empfohlen und seine Antwort zum `git push`-Riegel als Zustimmung zum Rest
|
||||||
|
gewertet. **Das war eine Annahme, keine Entscheidung.**
|
||||||
|
|
||||||
|
## Was damit wegfaellt — bewusst
|
||||||
|
|
||||||
|
| war | jetzt |
|
||||||
|
|---|---|
|
||||||
|
| Sperrliste: `rm -rf`, `sudo`, force push, `curl\|sh`, `ssh` ausser Arcane, `npm publish` | **kein Netz mehr** — der Agent darf alles, was die Shell hergibt |
|
||||||
|
| Pruef-Tor + Reparaturschleife (`VERIFY`) | schlief ohnehin, es gab nirgends eine `VERIFY`-Datei im PC-Baum |
|
||||||
|
| SAVEPOINT beim Komprimieren | weg — genau der Grund fuer den Abbau |
|
||||||
|
| Meldungen an Lucys Stimme | weg |
|
||||||
|
|
||||||
|
**Wiederherstellen** (wenn der fehlende Zaun sich raecht):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\"
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugins werden beim **Start des OpenCode-Servers** geladen — Aendern wirkt erst
|
||||||
|
nach einem Neustart von OpenChamber, nicht sofort. Deshalb liess sich das Plugin
|
||||||
|
auch mitten in einem laufenden Referenzlauf gefahrlos entfernen.
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/**
|
||||||
|
* MC2-Governor — der "Fahrlehrer" im OpenCode-Agenten.
|
||||||
|
*
|
||||||
|
* Der Governor-Proxy (:8100) ist die Tankuhr: er sieht nur Tokens und zieht die
|
||||||
|
* Notbremse. Dieses Plugin sitzt IM Agenten und sieht alles andere — jeden
|
||||||
|
* Werkzeuggriff, jede Datei, jedes Sitzungsende. Es macht vier Dinge:
|
||||||
|
*
|
||||||
|
* 1. WERKZEUG-ZAUN (tool.execute.before)
|
||||||
|
* Blockt Handgriffe, die ein Agent nie unbeaufsichtigt tun darf: push,
|
||||||
|
* Historie umschreiben, rekursiv loeschen, sudo, Fremd-Hosts. Genau dieser
|
||||||
|
* Zustandsautomat-Zaun hob lokale Modelle in Messungen von 2/10 auf 10/10 —
|
||||||
|
* nicht weil sie schlauer werden, sondern weil sie nicht mehr entgleisen.
|
||||||
|
*
|
||||||
|
* 2. PRUEF-TOR + SCHLEIFE (session.idle)
|
||||||
|
* Sagt der Agent "fertig", laeuft der Verify-Befehl des Projekts (Datei
|
||||||
|
* `VERIFY` im Repo-Wurzelverzeichnis). GRUEN -> Meldung. ROT -> der Fehler
|
||||||
|
* geht als naechster Auftrag automatisch zurueck an den Agenten, bis zu
|
||||||
|
* MC2_LOOP_MAX_ROUNDS mal. Das ist die "Ralph-Schleife", nur mit Bremse.
|
||||||
|
*
|
||||||
|
* 3. SAVEPOINT STATT ZUSAMMENFASSEN (session.compacted)
|
||||||
|
* Beim Komprimieren fallen still die Regeln aus dem Kontext (Paper
|
||||||
|
* "Governance Decay"). Wir schieben stattdessen den Auftrag nach, SAVEPOINT.md
|
||||||
|
* zu schreiben — Wissen lebt in Datei + git, nicht im schrumpfenden Chat.
|
||||||
|
*
|
||||||
|
* 4. STIMME (MC2 /api/voice/announce)
|
||||||
|
* Jedes Ereignis geht mit eigenem Absender `loop` in MC2s Melde-Briefkasten.
|
||||||
|
* Lucy pollt ihn ohnehin und spricht ihn — ohne eine Zeile Lucy-Code.
|
||||||
|
*
|
||||||
|
* Schalter (Umgebungsvariablen):
|
||||||
|
* MC2_BOX_URL MC2-Basis (Default http://192.168.178.151:9001)
|
||||||
|
* MC2_LOOP_AUTOFIX Selbstreparatur (1 = an, Default an)
|
||||||
|
* MC2_LOOP_MAX_ROUNDS max. Reparaturrunden (Default 3)
|
||||||
|
* MC2_LOOP_SILENT 1 = Lucy schweigt (Nachtlauf; Meldungen kommen trotzdem an)
|
||||||
|
* MC2_LOOP_ANNOUNCE 0 = gar keine Meldungen
|
||||||
|
* MC2_FENCE_OFF 1 = Werkzeug-Zaun aus (nur fuer Notfaelle)
|
||||||
|
*
|
||||||
|
* Liegt global unter ~/.config/opencode/plugin/ und wirkt damit in JEDEM Projekt —
|
||||||
|
* am Tag in Zed, nachts im Cron. Ein Regelwerk, zwei Ausloeser.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BOX_URL = process.env.MC2_BOX_URL || "http://192.168.178.151:9001"
|
||||||
|
const AUTOFIX = process.env.MC2_LOOP_AUTOFIX !== "0"
|
||||||
|
const MAX_ROUNDS = parseInt(process.env.MC2_LOOP_MAX_ROUNDS || "3", 10)
|
||||||
|
const SILENT = process.env.MC2_LOOP_SILENT === "1"
|
||||||
|
const ANNOUNCE_ON = process.env.MC2_LOOP_ANNOUNCE !== "0"
|
||||||
|
const FENCE_OFF = process.env.MC2_FENCE_OFF === "1"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbotene Shell-Handgriffe. Bewusst als Muster auf der ROHEN Kommandozeile —
|
||||||
|
* ein Agent, der `git push` in ein `bash -c` verpackt, wird trotzdem erwischt.
|
||||||
|
* Kein Anspruch auf Sandbox-Sicherheit: das ist ein Leitplanken-Zaun gegen
|
||||||
|
* Entgleisen, keine Abwehr gegen einen boesartigen Akteur.
|
||||||
|
*/
|
||||||
|
const FENCE: Array<{ rx: RegExp; why: string }> = [
|
||||||
|
// 21.08.2026: normales `git push` ist FREI. Der Weg ist jetzt PC -> Gitea -> Box, und
|
||||||
|
// jede Aenderung soll auch im git landen — ein Agent, der committen, aber nicht pushen
|
||||||
|
// darf, laesst Arbeit auf einem einzelnen Rechner liegen. Was schlecht ist, faengt
|
||||||
|
// hinterher die CI-Ampel; dieser Zaun muss es nicht vorher verhindern.
|
||||||
|
// Erzwungenes Pushen bleibt verboten: das ueberschreibt fremde Arbeit auf dem Server
|
||||||
|
// und ist genau die Klasse Unfall, gegen die der Zaun ueberhaupt existiert.
|
||||||
|
{ rx: /\bgit\s+push\b[^\n]*(--force(?!-with-lease)|\s-f\b|--mirror\b|--delete\b)/,
|
||||||
|
why: "git push --force/--mirror/--delete — ueberschreibt fremde Arbeit auf dem Server. Normales `git push` ist erlaubt; muss wirklich erzwungen werden, sag es dem Menschen." },
|
||||||
|
{ rx: /\bgit\s+reset\s+--hard\b/, why: "git reset --hard — verwirft Arbeit unwiederbringlich." },
|
||||||
|
{ rx: /\bgit\s+clean\s+-[a-z]*f/, why: "git clean -f — loescht ungetrackte Dateien unwiederbringlich." },
|
||||||
|
{ rx: /\bgit\s+(rebase|filter-branch|reflog\s+expire)\b/, why: "Historie umschreiben ist tabu." },
|
||||||
|
{ rx: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+\/(?:\s|$)/, why: "rm -rf / — nein." },
|
||||||
|
{ rx: /\brm\s+-[a-zA-Z]*[rf]/, why: "rekursives/erzwungenes Loeschen — bitte gezielt loeschen statt pauschal." },
|
||||||
|
{ rx: /\bsudo\b/, why: "sudo — Rechteausweitung gehoert nicht in einen Agentenlauf." },
|
||||||
|
{ rx: /\b(shutdown|reboot|mkfs|dd\s+if=)/, why: "System-/Datentraeger-Eingriff." },
|
||||||
|
{ rx: /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/, why: "Aus dem Netz laden und direkt ausfuehren — klassischer Fussschuss." },
|
||||||
|
{ rx: /\bssh\s+(?!arcane@192\.168\.178\.162|-o\s+StrictHostKeyChecking=no\s+arcane@)/, why: "ssh nur zur freigegebenen Arcane-VM." },
|
||||||
|
{ rx: /\bnpm\s+publish\b|\btwine\s+upload\b/, why: "Veroeffentlichen von Paketen ist Sache des Menschen." },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Zaehler je Sitzung: wie viele Selbstreparatur-Runden liefen schon? */
|
||||||
|
const rounds = new Map<string, number>()
|
||||||
|
/** Doppel-Feuern verhindern: session.idle kann mehrfach kommen. */
|
||||||
|
const busy = new Set<string>()
|
||||||
|
|
||||||
|
async function announce(subject: string, text: string, priority: "normal" | "silent" = "normal") {
|
||||||
|
if (!ANNOUNCE_ON) return
|
||||||
|
try {
|
||||||
|
await fetch(`${BOX_URL}/api/voice/announce`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
source: "loop",
|
||||||
|
priority: SILENT ? "silent" : priority,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(4000),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
/* best effort — eine stumme Lucy darf den Bau nie aufhalten */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MC2Governor = async ({ client, $, directory, worktree }: any) => {
|
||||||
|
const root: string = worktree || directory || process.cwd()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify-Befehl des Projekts lesen. Fehlt die Datei, ist das Pruef-Tor AUS.
|
||||||
|
* Bewusst ueber fs statt `cat`: das Plugin laeuft am Tag auf Windows (Zed) und
|
||||||
|
* nachts auf der Box — `cat` gibt es auf Windows nicht zuverlaessig.
|
||||||
|
*/
|
||||||
|
async function readVerify(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { readFile } = await import("node:fs/promises")
|
||||||
|
const { join } = await import("node:path")
|
||||||
|
const raw = await readFile(join(root, "VERIFY"), "utf8")
|
||||||
|
const cmd = raw
|
||||||
|
.split("\n")
|
||||||
|
.map((l: string) => l.trim())
|
||||||
|
.filter((l: string) => l && !l.startsWith("#"))
|
||||||
|
.join(" && ")
|
||||||
|
return cmd || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify ausfuehren. Rueckgabe: {ok, output} — Ausgabe auf das Wesentliche gekuerzt.
|
||||||
|
* `{ raw: cmd }` schiebt den Befehl UNESCAPED in Buns Shell; ein normales
|
||||||
|
* `${cmd}` wuerde die ganze Zeile als EIN Argument uebergeben und nie laufen.
|
||||||
|
* Buns Shell ist plattformunabhaengig — kein `bash -lc`, das auf Windows fehlt.
|
||||||
|
*/
|
||||||
|
async function runVerify(cmd: string): Promise<{ ok: boolean; out: string }> {
|
||||||
|
try {
|
||||||
|
const res = await $`${{ raw: cmd }}`.cwd(root).nothrow().quiet()
|
||||||
|
const out = `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`
|
||||||
|
return { ok: res.exitCode === 0, out: out.slice(-4000) }
|
||||||
|
} catch (e: any) {
|
||||||
|
return { ok: false, out: String(e?.message ?? e).slice(-4000) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dem laufenden Agenten einen neuen Auftrag schicken (Selbstreparatur-Schleife). */
|
||||||
|
async function sendPrompt(sessionID: string, text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await client.session.prompt({
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: { parts: [{ type: "text", text }] },
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ── 1. Werkzeug-Zaun ───────────────────────────────────────────────────
|
||||||
|
"tool.execute.before": async (input: any, output: any) => {
|
||||||
|
if (FENCE_OFF) return
|
||||||
|
if (input?.tool !== "bash") return
|
||||||
|
const cmd: string = output?.args?.command ?? ""
|
||||||
|
if (!cmd) return
|
||||||
|
for (const rule of FENCE) {
|
||||||
|
if (rule.rx.test(cmd)) {
|
||||||
|
await announce(
|
||||||
|
"[Zaun]",
|
||||||
|
`Ich habe einen Befehl geblockt: ${rule.why}`,
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
// Werfen = OpenCode bricht genau diesen Werkzeugaufruf ab und gibt dem
|
||||||
|
// Modell den Grund zurueck. Der Agent arbeitet weiter, nur anders.
|
||||||
|
throw new Error(
|
||||||
|
`[MC2-ZAUN] Blockiert: ${rule.why}\n` +
|
||||||
|
`Befehl war: ${cmd}\n` +
|
||||||
|
`Waehle einen anderen Weg. Wenn das wirklich noetig ist, sag es dem Menschen — ` +
|
||||||
|
`er macht es selbst.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 2.-4. Ereignisse ───────────────────────────────────────────────────
|
||||||
|
event: async ({ event }: any) => {
|
||||||
|
const type: string = event?.type ?? ""
|
||||||
|
const props: any = event?.properties ?? event ?? {}
|
||||||
|
const sessionID: string = props.sessionID || props.sessionId || props.id || ""
|
||||||
|
|
||||||
|
// ── Savepoint statt Zusammenfassen ──────────────────────────────────
|
||||||
|
if (type === "session.compacted" || type === "experimental.session.compacting") {
|
||||||
|
await announce(
|
||||||
|
"[Sitzung]",
|
||||||
|
"Die Sitzung wurde komprimiert — ich lasse den Stand in SAVEPOINT.md sichern.",
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
if (sessionID) {
|
||||||
|
await sendPrompt(
|
||||||
|
sessionID,
|
||||||
|
"[MC2-GOVERNOR] Der Kontext wurde gerade komprimiert — dabei gehen still " +
|
||||||
|
"Regeln und Details verloren. Aktualisiere JETZT SAVEPOINT.md: was wirklich " +
|
||||||
|
"erledigt ist (nur was im Code steht), der genaue naechste Schritt, offene " +
|
||||||
|
"Fragen, Stolpersteine. Committe die Datei. Danach arbeite normal weiter.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pruef-Tor + Selbstreparatur ─────────────────────────────────────
|
||||||
|
if (type !== "session.idle" || !sessionID) return
|
||||||
|
if (busy.has(sessionID)) return
|
||||||
|
|
||||||
|
const cmd = await readVerify()
|
||||||
|
if (!cmd) return // Kein VERIFY im Projekt -> Pruef-Tor bewusst aus.
|
||||||
|
|
||||||
|
busy.add(sessionID)
|
||||||
|
try {
|
||||||
|
const { ok, out } = await runVerify(cmd)
|
||||||
|
const round = rounds.get(sessionID) ?? 0
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
rounds.delete(sessionID)
|
||||||
|
await announce("[Pruefung]", "Etappe fertig und die Tests sind gruen.", "normal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AUTOFIX || round >= MAX_ROUNDS) {
|
||||||
|
rounds.delete(sessionID)
|
||||||
|
await announce(
|
||||||
|
"[Pruefung]",
|
||||||
|
`Die Tests sind rot und ich habe ${round} Reparaturversuche verbraucht. ` +
|
||||||
|
`Hier komme ich allein nicht weiter, Commander.`,
|
||||||
|
"normal",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rounds.set(sessionID, round + 1)
|
||||||
|
await announce(
|
||||||
|
"[Pruefung]",
|
||||||
|
`Tests rot — ich repariere selbst weiter, Runde ${round + 1} von ${MAX_ROUNDS}.`,
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
await sendPrompt(
|
||||||
|
sessionID,
|
||||||
|
`[MC2-PRUEFTOR] Deine Etappe gilt noch NICHT als fertig: der Verify-Befehl des ` +
|
||||||
|
`Projekts ist fehlgeschlagen.\n\n` +
|
||||||
|
`Befehl: ${cmd}\n\n` +
|
||||||
|
`Ausgabe (Ende):\n\`\`\`\n${out}\n\`\`\`\n\n` +
|
||||||
|
`Behebe die Ursache — nicht das Symptom, und schalte keinen Test ab. ` +
|
||||||
|
`Wenn du fertig bist, melde dich normal; ich pruefe dann erneut. ` +
|
||||||
|
`(Reparaturrunde ${round + 1} von ${MAX_ROUNDS}.)`,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
busy.delete(sessionID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MC2Governor
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Projekte-Sync (20.08.2026): haelt ~/projekte mit Gitea deckungsgleich.
|
||||||
|
#
|
||||||
|
# Warum: Der Hermes-Desktop laeuft gegen das Box-Backend, sein Ordner-Waehler zeigt
|
||||||
|
# also das Dateisystem der BOX. Damit dort alle Projekte auftauchen, muessen sie hier
|
||||||
|
# liegen — und aktuell sein.
|
||||||
|
#
|
||||||
|
# Regeln (bewusst konservativ — dieses Skript laeuft unbeaufsichtigt):
|
||||||
|
# • NUR fast-forward (`--ff-only`). Kein merge, kein rebase, kein reset, kein force.
|
||||||
|
# • Repos mit lokalen Aenderungen werden UEBERSPRUNGEN, nicht ueberfahren.
|
||||||
|
# • Live-Deployments werden NIE gezogen (mission-control-v2 wird aus dem Deploy
|
||||||
|
# bedient und pusht selbst) — sie bekommen nur eine Verknuepfung, damit sie im
|
||||||
|
# Waehler auftauchen.
|
||||||
|
# • Bestehende Checkouts in ~ bleiben, wo sie sind; ~/projekte bekommt eine
|
||||||
|
# Verknuepfung darauf statt eines zweiten Klons.
|
||||||
|
# • Interne Gitea-IP, NICHT die DDNS-Domain: die ist nachts durch die
|
||||||
|
# Zwangstrennung zeitweise tot (Lehre vom 24.07.).
|
||||||
|
#
|
||||||
|
# Aufruf: projekte-sync.sh [--dry-run]
|
||||||
|
# Exit: 0 = alles gut · 1 = mindestens ein Repo hatte ein Problem
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
||||||
|
ZIEL="${PROJEKTE_DIR:-$HOME/projekte}"
|
||||||
|
LOG="${PROJEKTE_SYNC_LOG:-$HOME/.hermes/logs/projekte-sync.log}"
|
||||||
|
DRY=0
|
||||||
|
[ "${1:-}" = "--dry-run" ] && DRY=1
|
||||||
|
|
||||||
|
# Repos, die NICHT nach ~/projekte geklont werden (Live-Deployment oder Altlast).
|
||||||
|
# mission-control-v2: laeuft aus ~/mission-control-v2, Dienste arbeiten daraus.
|
||||||
|
# mission-control: archivierte v1.
|
||||||
|
NIE_ZIEHEN="mission-control-v2"
|
||||||
|
UEBERSPRINGEN="mission-control"
|
||||||
|
|
||||||
|
mkdir -p "$ZIEL" "$(dirname "$LOG")"
|
||||||
|
FEHLER=0
|
||||||
|
|
||||||
|
log() { printf '%s %s\n' "$(date '+%F %T')" "$*" | tee -a "$LOG"; }
|
||||||
|
|
||||||
|
TOKEN=""
|
||||||
|
for f in "$HOME/.config/gitea/create-token"; do
|
||||||
|
[ -r "$f" ] && TOKEN="$(tr -d '[:space:]' < "$f")" && break
|
||||||
|
done
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
log "ABBRUCH: kein Gitea-Token unter ~/.config/gitea/create-token"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPOS="$(curl -s -m 20 -H "Authorization: token $TOKEN" \
|
||||||
|
"$GITEA/api/v1/user/repos?limit=100" \
|
||||||
|
| python3 -c 'import sys,json; [print(r["name"]) for r in json.load(sys.stdin)]' 2>/dev/null)"
|
||||||
|
if [ -z "$REPOS" ]; then
|
||||||
|
log "ABBRUCH: Gitea lieferte keine Repo-Liste ($GITEA)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "=== Sync-Lauf ($(printf '%s\n' "$REPOS" | wc -l) Repos gemeldet)$([ $DRY -eq 1 ] && echo ' [Trockenlauf]') ==="
|
||||||
|
|
||||||
|
# Vorhandenen Checkout eines Repos finden (oberste Home-Ebene bevorzugt).
|
||||||
|
finde_checkout() {
|
||||||
|
local name="$1" d
|
||||||
|
for d in "$HOME/$name" "$ZIEL/$name"; do
|
||||||
|
[ -d "$d/.git" ] && { printf '%s' "$d"; return 0; }
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
verknuepfe() { # $1=Zielpfad des echten Checkouts, $2=Repo-Name
|
||||||
|
local ziel="$1" name="$2" link="$ZIEL/$name"
|
||||||
|
[ "$ziel" = "$link" ] && return 0
|
||||||
|
[ -L "$link" ] && [ "$(readlink -f "$link")" = "$(readlink -f "$ziel")" ] && return 0
|
||||||
|
[ -e "$link" ] && [ ! -L "$link" ] && { log " ! $name: $link existiert und ist keine Verknuepfung — nichts getan"; return 1; }
|
||||||
|
[ $DRY -eq 1 ] && { log " ~ $name: wuerde verknuepfen -> $ziel"; return 0; }
|
||||||
|
ln -sfn "$ziel" "$link" && log " ~ $name: verknuepft -> ${ziel/$HOME/\~}"
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in $REPOS; do
|
||||||
|
case " $UEBERSPRINGEN " in *" $name "*) log " - $name: uebersprungen (Altlast)"; continue;; esac
|
||||||
|
|
||||||
|
if checkout="$(finde_checkout "$name")"; then
|
||||||
|
case " $NIE_ZIEHEN " in
|
||||||
|
*" $name "*)
|
||||||
|
log " = $name: Live-Deployment, kein Pull"
|
||||||
|
verknuepfe "$checkout" "$name"
|
||||||
|
continue;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "$(git -C "$checkout" status --porcelain 2>/dev/null)" ]; then
|
||||||
|
log " ! $name: lokale Aenderungen — uebersprungen (nichts ueberfahren)"
|
||||||
|
verknuepfe "$checkout" "$name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $DRY -eq 1 ]; then
|
||||||
|
log " ~ $name: wuerde pullen (${checkout/$HOME/\~})"
|
||||||
|
else
|
||||||
|
vorher="$(git -C "$checkout" rev-parse --short HEAD 2>/dev/null)"
|
||||||
|
if out="$(git -C "$checkout" pull --ff-only 2>&1)"; then
|
||||||
|
nachher="$(git -C "$checkout" rev-parse --short HEAD 2>/dev/null)"
|
||||||
|
if [ "$vorher" = "$nachher" ]; then
|
||||||
|
log " = $name: aktuell ($vorher)"
|
||||||
|
else
|
||||||
|
log " + $name: $vorher -> $nachher"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log " ! $name: Pull fehlgeschlagen — $(printf '%s' "$out" | tail -1)"
|
||||||
|
FEHLER=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
verknuepfe "$checkout" "$name"
|
||||||
|
else
|
||||||
|
if [ $DRY -eq 1 ]; then
|
||||||
|
log " ~ $name: wuerde klonen nach ${ZIEL/$HOME/\~}/$name"
|
||||||
|
elif out="$(git clone -q "$GITEA/Hitonabi/$name.git" "$ZIEL/$name" 2>&1)"; then
|
||||||
|
log " + $name: neu geklont"
|
||||||
|
else
|
||||||
|
log " ! $name: Klonen fehlgeschlagen — $(printf '%s' "$out" | tail -1)"
|
||||||
|
FEHLER=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log "=== Ende (Fehler: $FEHLER) ==="
|
||||||
|
exit $FEHLER
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<#
|
||||||
|
push-und-sync.ps1 — der Handschlag zwischen PC und AI-Box (21.08.2026).
|
||||||
|
|
||||||
|
Bahn: PC (Quelle der Wahrheit) -> Gitea -> Box ~/projekte
|
||||||
|
|
||||||
|
Warum ueberhaupt ein Skript: `projekte-sync` laeuft auf der Box stuendlich. Wer
|
||||||
|
gerade gepusht hat, will aber nicht bis zu 60 Minuten warten. Das hier stoesst
|
||||||
|
denselben Dienst sofort an — kein zweiter Mechanismus, nur ein Ausloeser.
|
||||||
|
|
||||||
|
Warum PowerShell und nicht Git-Bash: Push zum Gitea geht vom PC nur ueber den
|
||||||
|
Git Credential Manager, und den gibt es hier (Lehre vom 24.07.).
|
||||||
|
|
||||||
|
Aufruf (im Projektordner):
|
||||||
|
.\deploy\push-und-sync.ps1
|
||||||
|
.\deploy\push-und-sync.ps1 -NurSync # nicht pushen, nur Box nachziehen
|
||||||
|
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string] $Pfad = (Get-Location).Path,
|
||||||
|
[switch] $NurSync,
|
||||||
|
[string] $BoxHost = "hitonabi@192.168.178.151"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Schritt($text) { Write-Host "`n== $text" -ForegroundColor Cyan }
|
||||||
|
function Gut($text) { Write-Host " $text" -ForegroundColor Green }
|
||||||
|
function Warn($text) { Write-Host " $text" -ForegroundColor Yellow }
|
||||||
|
|
||||||
|
Set-Location $Pfad
|
||||||
|
|
||||||
|
# --- 1. Ist das ueberhaupt ein Repo, und was steht an? ---------------------
|
||||||
|
$null = git rev-parse --is-inside-work-tree 2>$null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Kein Git-Repository: $Pfad" }
|
||||||
|
|
||||||
|
$zweig = (git branch --show-current).Trim()
|
||||||
|
|
||||||
|
# Repo-Name aus der Remote-URL, NICHT aus dem Ordnernamen: der lokale Ordner heisst
|
||||||
|
# z. B. "mission-control-2", das Gitea-Repo aber "mission-control-v2". Der Ordnername
|
||||||
|
# haette die Box-Abfrage still ins Leere laufen lassen.
|
||||||
|
$remote = (git remote get-url origin).Trim()
|
||||||
|
$repo = [System.IO.Path]::GetFileNameWithoutExtension(($remote -split '/')[-1])
|
||||||
|
Schritt "Repo '$repo', Zweig '$zweig'"
|
||||||
|
|
||||||
|
$offen = git status --porcelain
|
||||||
|
if ($offen) {
|
||||||
|
Warn "Es liegen unversionierte/ungespeicherte Aenderungen:"
|
||||||
|
$offen | Select-Object -First 10 | ForEach-Object { Warn " $_" }
|
||||||
|
Warn "Diese werden NICHT mitgeschickt. Erst committen, dann erneut aufrufen."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 2. Pushen ------------------------------------------------------------
|
||||||
|
if (-not $NurSync) {
|
||||||
|
$ausstehend = git log --oneline "origin/$zweig..HEAD" 2>$null
|
||||||
|
if (-not $ausstehend) {
|
||||||
|
Gut "Nichts zu pushen — Gitea ist bereits auf Stand."
|
||||||
|
} else {
|
||||||
|
Schritt "Push nach Gitea ($(($ausstehend | Measure-Object).Count) Commit(s))"
|
||||||
|
$ausstehend | ForEach-Object { Write-Host " $_" }
|
||||||
|
git push origin $zweig
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw @"
|
||||||
|
Push fehlgeschlagen.
|
||||||
|
Seit 21.08. laeuft der Weg ueber SSH: ssh://gitea@192.168.178.153:2222/...
|
||||||
|
Pruefen mit: ssh -T gitea
|
||||||
|
Kommt dort keine Begruessung, lauscht Giteas SSH-Server nicht (Port 2222,
|
||||||
|
START_SSH_SERVER in /etc/gitea/app.ini) oder id_general ist nicht hinterlegt.
|
||||||
|
Der SSH-Benutzer heisst 'gitea', NICHT 'git' - Gitea laeuft unter diesem Namen.
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
Gut "gepusht"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 3. Box nachziehen lassen --------------------------------------------
|
||||||
|
Schritt "Box zieht nach (projekte-sync)"
|
||||||
|
ssh $BoxHost "systemctl --user start projekte-sync.service"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Sync-Dienst liess sich nicht starten." }
|
||||||
|
|
||||||
|
$stand = ssh $BoxHost "cd ~/projekte/$repo 2>/dev/null && git log --oneline -1 || echo 'NICHT-IN-PROJEKTE'"
|
||||||
|
if ($stand -eq "NICHT-IN-PROJEKTE") {
|
||||||
|
Warn "'$repo' liegt nicht unter ~/projekte."
|
||||||
|
Warn "Bei mission-control-v2 ist das ABSICHT: das Live-Deployment liegt in ~/mission-control-v2"
|
||||||
|
Warn "und wird bewusst nicht automatisch gezogen — es braucht seinen eigenen Deploy-Schritt."
|
||||||
|
} else {
|
||||||
|
Gut "Box steht jetzt auf: $stand"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
+6
-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/"; }
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ heavy-Rollen einer Runde am Stueck (heavy bleibt warm), DANN der Skeptiker (ein
|
|||||||
aus — fuer diesen Hintergrund-Job gewollt. heavy hat nur **32k Kontext** → halte das Konzept-
|
aus — fuer diesen Hintergrund-Job gewollt. heavy hat nur **32k Kontext** → halte das Konzept-
|
||||||
Dokument kompakt und schleppe KEINE alten Runden-Protokolle mit (siehe Kontext-Uebergabe).
|
Dokument kompakt und schleppe KEINE alten Runden-Protokolle mit (siehe Kontext-Uebergabe).
|
||||||
- **REASONING-MODELLE brauchen Token-Luft (verifiziert 14.07.).** heavy (gpt-oss) UND der Skeptiker
|
- **REASONING-MODELLE brauchen Token-Luft (verifiziert 14.07.).** heavy (gpt-oss) UND der Skeptiker
|
||||||
(GLM-4.7-Flash) „denken" erst ausfuehrlich (im `reasoning_content`), bevor die eigentliche Antwort
|
(`fast`) „denken" erst ausfuehrlich (im `reasoning_content`), bevor die eigentliche Antwort
|
||||||
in `content` landet — `worker.sh`/`fremdblick.sh` lesen NUR `content`. Zu knappes `max_tokens` =
|
in `content` landet — `worker.sh`/`fremdblick.sh` lesen NUR `content`. Zu knappes `max_tokens` =
|
||||||
das Modell verbraucht alles beim Denken, `content` bleibt LEER, das Skript meldet „FEHLGESCHLAGEN"
|
das Modell verbraucht alles beim Denken, `content` bleibt LEER, das Skript meldet „FEHLGESCHLAGEN"
|
||||||
(obwohl das Modell lief). GLM verbrennt real ~2000 Tokens Denken fuer 2 Zeilen Antwort. Deshalb
|
(obwohl das Modell lief). GLM verbrennt real ~2000 Tokens Denken fuer 2 Zeilen Antwort. Deshalb
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ Vorschlag ausweisen, was fehlt oder wacklig ist.
|
|||||||
NIEMALS auf `main` committen, NIEMALS mergen, NIEMALS deployen oder Dienste neu starten.
|
NIEMALS auf `main` committen, NIEMALS mergen, NIEMALS deployen oder Dienste neu starten.
|
||||||
- **Nur im Worktree arbeiten.** Die Live-Instanz `~/mission-control-v2` bleibt unberuehrt.
|
- **Nur im Worktree arbeiten.** Die Live-Instanz `~/mission-control-v2` bleibt unberuehrt.
|
||||||
- **Warm-Set-Schutz (Speicher vs Latenz).** Worker werden ueber **`worker.sh`** aufgerufen.
|
- **Warm-Set-Schutz (Speicher vs Latenz).** Worker werden ueber **`worker.sh`** aufgerufen.
|
||||||
Fuer die Umsetzung von Code nutzt du **`Qwen3-Coder-Next`** (laedt klein neben dem Warm-Set).
|
Fuer die Umsetzung von Code nutzt du **`coder`** (laedt klein neben dem Warm-Set).
|
||||||
Fuer die initiale **Planung** nutzt du explizit den Chef-Gutachter **`gpt-oss-120b`** (heavy),
|
Fuer die initiale **Planung** nutzt du explizit den Chef-Gutachter **`gpt-oss-120b`** (heavy),
|
||||||
auch wenn dessen Laden (~15s) Lucys Sprech-Latenz temporaer aussetzt. Das ist fuer diesen
|
auch wenn dessen Laden (~15s) Lucys Sprech-Latenz temporaer aussetzt. Das ist fuer diesen
|
||||||
Hintergrund-Prozess gewollt.
|
Hintergrund-Prozess gewollt.
|
||||||
@@ -83,7 +83,7 @@ Bevor du selbst Tasks aufteilst, befragst du zwingend **`gpt-oss-120b`** nach ei
|
|||||||
printf '%s' "Auftrag: $auftrag" | WORKER_MODEL=gpt-oss-120b WORKER_ROLE=plan ~/.hermes/scripts/worker.sh > /tmp/orch-plan.txt
|
printf '%s' "Auftrag: $auftrag" | WORKER_MODEL=gpt-oss-120b WORKER_ROLE=plan ~/.hermes/scripts/worker.sh > /tmp/orch-plan.txt
|
||||||
```
|
```
|
||||||
2. Lies den Plan (`cat /tmp/orch-plan.txt`) und nutze ihn, um den Auftrag in eine **geordnete, nummerierte Liste von Teil-Tasks** zu zerlegen.
|
2. Lies den Plan (`cat /tmp/orch-plan.txt`) und nutze ihn, um den Auftrag in eine **geordnete, nummerierte Liste von Teil-Tasks** zu zerlegen.
|
||||||
3. Fuer jeden Teil-Task entscheide: (a) Artefakt, (b) Worker (`Qwen3-Coder-Next` + `build`/`refactor`), (c) Kontext.
|
3. Fuer jeden Teil-Task entscheide: (a) Artefakt, (b) Worker (`coder` + `build`/`refactor`), (c) Kontext.
|
||||||
|
|
||||||
### 2. Worktree anlegen (Slug = kurzer Kebab-Case-Name des Auftrags)
|
### 2. Worktree anlegen (Slug = kurzer Kebab-Case-Name des Auftrags)
|
||||||
Worktrees liegen unter `~/.hermes/worktrees/` (NICHT /tmp — ueberlebt keinen Reboot und
|
Worktrees liegen unter `~/.hermes/worktrees/` (NICHT /tmp — ueberlebt keinen Reboot und
|
||||||
@@ -102,7 +102,7 @@ Fuer jeden Bau-Teil-Task:
|
|||||||
```
|
```
|
||||||
printf '%s' "$eingabe" | WORKER_ROLE=build ~/.hermes/scripts/worker.sh > ~/.hermes/worktrees/orch-<slug>-work/<n>.out
|
printf '%s' "$eingabe" | WORKER_ROLE=build ~/.hermes/scripts/worker.sh > ~/.hermes/worktrees/orch-<slug>-work/<n>.out
|
||||||
```
|
```
|
||||||
(Default-Modell ist `Qwen3-Coder-Next`. Fuer einen bewusst anderen Worker `WORKER_MODEL=<echte-id>`.)
|
(Default-Modell ist `coder`. Fuer einen bewusst anderen Worker `WORKER_MODEL=<rolle-oder-id>`.)
|
||||||
3. **Pruefe die Ausgabe, bevor du sie verwendest:** Beginnt sie mit `FEHLT:`, hast du zu wenig
|
3. **Pruefe die Ausgabe, bevor du sie verwendest:** Beginnt sie mit `FEHLT:`, hast du zu wenig
|
||||||
Kontext gegeben → nachliefern und erneut rufen. Hat der Worker versehentlich einen ```-Codezaun
|
Kontext gegeben → nachliefern und erneut rufen. Hat der Worker versehentlich einen ```-Codezaun
|
||||||
oder Prosa drumherum gesetzt, entferne ihn. Dann schreibe das saubere Artefakt mit deinen
|
oder Prosa drumherum gesetzt, entferne ihn. Dann schreibe das saubere Artefakt mit deinen
|
||||||
@@ -164,13 +164,13 @@ GIT-DIFF des Worktrees:
|
|||||||
$(git -C ~/.hermes/worktrees/orch-<slug> diff --cached origin/main)"
|
$(git -C ~/.hermes/worktrees/orch-<slug> diff --cached origin/main)"
|
||||||
```
|
```
|
||||||
Ist dieser Diff LEER, hast du nicht gestaged (oder nichts gebaut) → NICHT weiter, erst beheben.
|
Ist dieser Diff LEER, hast du nicht gestaged (oder nichts gebaut) → NICHT weiter, erst beheben.
|
||||||
**Kritik A — Kompetenz** (`Qwen3-Coder-Next`, starker Coder, vom Bauen noch geladen → KEIN Swap):
|
**Kritik A — Kompetenz** (`coder`, starker Coder, vom Bauen noch geladen → KEIN Swap):
|
||||||
```
|
```
|
||||||
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||||
```
|
```
|
||||||
**Kritik B — Fremd-Blick** (`GLM-4.6V-Flash`, andere Modellfamilie, andere blinde Flecken):
|
**Kritik B — Fremd-Blick** (`debugger`, andere Modellfamilie, andere blinde Flecken):
|
||||||
```
|
```
|
||||||
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code FREMDBLICK_MODEL=GLM-4.6V-Flash FREMDBLICK_MAXTOK=3000 ~/.hermes/scripts/fremdblick.sh
|
printf '%s' "$EINGABE" | FREMDBLICK_MODE=code FREMDBLICK_MODEL=debugger FREMDBLICK_MAXTOK=3000 ~/.hermes/scripts/fremdblick.sh
|
||||||
```
|
```
|
||||||
Reihenfolge bewusst: erst A (Coder-Next ist noch warm), dann B (ein Swap zu GLM). Beide beginnen mit
|
Reihenfolge bewusst: erst A (Coder-Next ist noch warm), dann B (ein Swap zu GLM). Beide beginnen mit
|
||||||
`URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT | FREIGABE>`. **Warte auf beide.**
|
`URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT | FREIGABE>`. **Warte auf beide.**
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ unangenommene Branches aufbauen — erst wenn die Vor-Etappe in main ist, kommt
|
|||||||
<git -C ~/.hermes/worktrees/wartung-<slug> diff origin/main>" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
<git -C ~/.hermes/worktrees/wartung-<slug> diff origin/main>" | FREMDBLICK_MODE=code ~/.hermes/scripts/fremdblick.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Das läuft auf `Qwen3-Coder-Next` (128k, code-stark, anderes Modell als der Qwen3.6-Worker,
|
Das läuft auf `coder` (128k, code-stark, anderes Modell als der Qwen3.6-Worker,
|
||||||
lädt klein neben dem Warm-Set) mit einem Code-Review-Raster. **Warte auf die Ausgabe.** Der
|
lädt klein neben dem Warm-Set) mit einem Code-Review-Raster. **Warte auf die Ausgabe.** Der
|
||||||
Kritiker leitet den Fehlerfall SELBST aus der Bug-Beschreibung ab (glaubt dem Autor nicht),
|
Kritiker leitet den Fehlerfall SELBST aus der Bug-Beschreibung ab (glaubt dem Autor nicht),
|
||||||
spielt den geänderten Code durch und beginnt mit `URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT
|
spielt den geänderten Code durch und beginnt mit `URTEIL: <ABGELEHNT | FREIGABE-MIT-VORBEHALT
|
||||||
|
|||||||
@@ -3,15 +3,14 @@
|
|||||||
# dass der komplette Inferenz-Stack noch FUNKTIONIERT — nicht nur "Befehl lief durch".
|
# 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,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.
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@
|
|||||||
# neben dem Warm-Set liegt bewusst in DEINER Hand.
|
# neben dem Warm-Set liegt bewusst in DEINER Hand.
|
||||||
#
|
#
|
||||||
# WICHTIG (Speicher & Latenz-Trade-off):
|
# WICHTIG (Speicher & Latenz-Trade-off):
|
||||||
# Der Standard-Worker ist Qwen3-Coder-Next (laedt klein und schnell neben dem Warm-Set).
|
# Der Standard-Worker ist die Rolle `coder` (laedt klein und schnell neben dem Warm-Set).
|
||||||
# Fuer initiale Planung kann "gpt-oss-120b" gerufen werden. Das Modell passt knapp in den
|
# Fuer initiale Planung kann "gpt-oss-120b" gerufen werden. Das Modell passt knapp in den
|
||||||
# Speicher (124 GB RAM), aber das on-demand Laden unterbricht Lucys 1-s-Latenz fuer ca.
|
# Speicher (124 GB RAM), aber das on-demand Laden unterbricht Lucys 1-s-Latenz fuer ca.
|
||||||
# 15-30 Sekunden. Dieser Latenz-Hit wird fuer die Orchestrator-Planung in Kauf genommen.
|
# 15-30 Sekunden. Dieser Latenz-Hit wird fuer die Orchestrator-Planung in Kauf genommen.
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
# in Dateien macht der Manager (Hermes) danach mit seinen eigenen Tools.
|
# in Dateien macht der Manager (Hermes) danach mit seinen eigenen Tools.
|
||||||
#
|
#
|
||||||
# Nutzung: printf '%s' "$teiltask_mit_kontext" | WORKER_ROLE=build worker.sh > /tmp/out.py
|
# Nutzung: printf '%s' "$teiltask_mit_kontext" | WORKER_ROLE=build worker.sh > /tmp/out.py
|
||||||
# printf '%s' "$spec" | WORKER_MODEL=Qwen3-Coder-Next WORKER_ROLE=build worker.sh
|
# printf '%s' "$spec" | WORKER_MODEL=coder WORKER_ROLE=build worker.sh
|
||||||
#
|
#
|
||||||
# Env-Overrides:
|
# Env-Overrides:
|
||||||
# WORKER_MODEL echte llama-swap-ID (Default: Qwen3-Coder-Next). NICHT gpt-oss/heavy (siehe oben).
|
# WORKER_MODEL Rollen-Alias oder echte ID (Default: coder). NICHT gpt-oss/heavy (siehe oben).
|
||||||
# WORKER_ROLE build (Default) | refactor | plan | prose — waehlt das System-Raster.
|
# WORKER_ROLE build (Default) | refactor | plan | prose — waehlt das System-Raster.
|
||||||
# WORKER_SYS eigenes System-Raster (uebersteuert WORKER_ROLE).
|
# WORKER_SYS eigenes System-Raster (uebersteuert WORKER_ROLE).
|
||||||
# WORKER_MAXTOK max_tokens (Default 4000 — Code braucht Platz).
|
# WORKER_MAXTOK max_tokens (Default 4000 — Code braucht Platz).
|
||||||
@@ -29,7 +29,7 @@ set -uo pipefail
|
|||||||
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
|
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
|
||||||
# (kein "heavy"/"coder"/"scout" hier — die echte ID nehmen).
|
# (kein "heavy"/"coder"/"scout" hier — die echte ID nehmen).
|
||||||
ENDPOINT="${WORKER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
ENDPOINT="${WORKER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||||
MODEL="${WORKER_MODEL:-Qwen3-Coder-Next}"
|
MODEL="${WORKER_MODEL:-coder}"
|
||||||
ROLE="${WORKER_ROLE:-build}"
|
ROLE="${WORKER_ROLE:-build}"
|
||||||
MAXTOK="${WORKER_MAXTOK:-4000}"
|
MAXTOK="${WORKER_MAXTOK:-4000}"
|
||||||
TEMP="${WORKER_TEMP:-0.1}"
|
TEMP="${WORKER_TEMP:-0.1}"
|
||||||
|
|||||||
+13
-16
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **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**.
|
||||||
>
|
>
|
||||||
@@ -17,8 +17,8 @@ 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
|
||||||
@@ -73,18 +73,15 @@ Legende Restore-Quelle: 📦 = aus Backup-Tarball · ⬇️ = Re-Download/Instal
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 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? |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Default-Avatar (VRM) | `frontend/public/avatar.vrm` (→ dist) | 🌐 Git | n/a |
|
|
||||||
| Renderer | `frontend/src/components/voice/Avatar3D.tsx` | 🌐 Git | n/a |
|
|
||||||
| **Eigene/zusätzliche Avatare** (falls Nutzer welche ablegt) | **TODO: Ablageort definieren** (z.B. `/srv/models/avatars/` + DB-Verweis) | 📦/🆕 | **nein (Lücke)** |
|
|
||||||
|
|
||||||
> Heute ist der Avatar **fest** (`avatar.vrm`, kommt mit dem Git-Frontend zurück). Wenn künftig
|
MC2 hat keinen Avatar mehr. `frontend/public/avatar.vrm` (24,5 MB) lag im Auslieferungsordner,
|
||||||
> Nutzer-Avatare hochgeladen werden, brauchen sie einen persistenten Ablageort, der ins Backup geht.
|
wurde aber von keiner Zeile des Frontends referenziert — der Renderer `Avatar3D.tsx` war schon
|
||||||
|
vorher verschwunden. Beides ist beim v3-Umbau (Etappe P0) entfernt worden; damit fällt auch die
|
||||||
|
`.gitignore`-Sonderregel und der Direkt-Deploy-Schritt weg. **Nichts wiederherzustellen.**
|
||||||
|
|
||||||
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
### E · Voice-Sidecar (STT + TTS + Klonstimme)
|
||||||
| Komponente | Ort | Quelle | Im Backup? |
|
| Komponente | Ort | Quelle | Im Backup? |
|
||||||
@@ -176,7 +173,7 @@ MC2 erkennt unkonfigurierten Zustand → Frontend-Route `/setup` statt Dashboard
|
|||||||
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
ElevenLabs-Key → `~/.hermes/.env` (chmod 600). Bei 📦 vorbefüllt aus Backup.
|
||||||
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
6. **Hermes-Profil** — Brain-Alias + Toolset-Profil (Default = entfesselt: vision/tts/memory/browser
|
||||||
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
an, image_gen/video aus — siehe [[project-hermes-setup]]). Bei 📦 = exakte alte `config.yaml`.
|
||||||
7. **Avatar & Voice** — Avatar wählen (Default-VRM oder eigener), Stimme/Klonstimme
|
7. **Voice** — Stimme/Klonstimme
|
||||||
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
(📦 Referenz-Audio zurück, oder neu aufnehmen/hochladen).
|
||||||
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
8. **Modelle** — aus llama-swap-Manifest automatisch nachladen (`POST /api/models/install`,
|
||||||
Fortschritt `GET /api/jobs`) ODER geführte Discover-Neuauswahl. Reihenfolge: Brain → heavy → Rest.
|
Fortschritt `GET /api/jobs`) ODER geführte Discover-Neuauswahl. Reihenfolge: Brain → heavy → Rest.
|
||||||
@@ -202,7 +199,7 @@ liegt das **fertige Erweiterungs-Snippet in Anhang B** — es ergänzt den Tarba
|
|||||||
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
Restore soll skills/sessions **mergen** (frisch geladenen `.hub` nicht überschreiben) und `voice-service`
|
||||||
mit neu starten.
|
mit neu starten.
|
||||||
|
|
||||||
**Offen bleibt:** eigene Avatare (sobald Upload existiert — Ablageort heute undefiniert, siehe §3·D).
|
**Offen bleibt:** nichts — der letzte offene Punkt (eigene Avatare) ist mit dem Avatar-Ausbau erledigt (§3·D).
|
||||||
|
|
||||||
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
**Bewusst NICHT im Backup (re-downloadbar/rebuildbar):** Piper-Stimmen (install.sh), `.hub`-Skill-Cache,
|
||||||
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
`/srv/models/mc2-discover.json` (Cache), `/srv/models/mc2-memory.db` (Legacy-Migration), alle venvs, GGUF-Modelle.
|
||||||
@@ -239,7 +236,7 @@ Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
|||||||
|
|
||||||
## 9. Abnahmekriterien
|
## 9. Abnahmekriterien
|
||||||
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
- Frisches Ubuntu 26.04 → `bootstrap-root.sh` + `bootstrap.sh` → laufender Stack, kein Spezialwissen.
|
||||||
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), 3D-Avatar, Browser funktionieren.
|
- Postchecks grün; Telegram, Voice (inkl. Klonstimme bei 📦), Browser funktionieren.
|
||||||
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
- „Alles 1:1" stellt mem0, Hermes-config.yaml, Secrets, Klonstimme, Skills exakt wieder her.
|
||||||
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
- Selektiver Modus: einzelne Komponenten ⏭️ überspringbar, Stack läuft trotzdem.
|
||||||
- Idempotenz: zweiter Lauf = No-op.
|
- Idempotenz: zweiter Lauf = No-op.
|
||||||
@@ -247,7 +244,7 @@ Vorschlag: 2–4 zuerst (Skelett lauffähig), Wizard danach. Branch + PR.
|
|||||||
## 10. Offene Entscheidungen (vor dem Bau)
|
## 10. Offene Entscheidungen (vor dem Bau)
|
||||||
1. ~~llama-swap systemd-Unit-Inhalt~~ → **geklärt: kompletter Unit in Anhang A** (in der Bau-Session anlegen).
|
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).
|
2. ~~Voice-Referenz-Audio-Ort~~ → **geklärt: `~/.voice/refs/`** (Backup-Snippet in Anhang B, in der Bau-Session umsetzen).
|
||||||
3. **Eigene Avatare**: künftiger Upload-/Ablage-Mechanismus + Backup-Pfad (einziger offener 1:1-Daten-Punkt).
|
3. ~~**Eigene Avatare**~~ — entfallen: Avatar am 28.08.2026 ausgebaut (§3·D).
|
||||||
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
4. **Off-Box-Backup-Ziel** (NAS/2. Platte/Cloud) — [[mc2-backup-restore]].
|
||||||
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
5. **Python 3.14** auf frischem Ubuntu beschaffen (deadsnakes?).
|
||||||
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
6. **Bootstrap-sudo-Modell**: getrenntes `bootstrap-root.sh` (empfohlen) vs. interaktives sudo.
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Referenz-Check (20.08.2026): prueft die fuenf Fertig-Kriterien der Messlatte.
|
||||||
|
#
|
||||||
|
# Zweck: "fertig" darf keine Auslegungssache sein. Derselbe Befehl fuer den Agenten
|
||||||
|
# (Selbstpruefung) und fuer die Auswertung. Siehe docs/aufgaben/referenzaufgabe-mem0-ausbau.md
|
||||||
|
#
|
||||||
|
# Aufruf: bash docs/aufgaben/referenz-check.sh [worktree-pfad]
|
||||||
|
# Exit: 0 = alle Kriterien gruen · 1 = mindestens eines rot
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
W="${1:-$HOME/projekte/mc2-referenz}"
|
||||||
|
LINT="${LINT_RUFF:-$HOME/.venv-lint/bin/ruff}"
|
||||||
|
PY="${BACKEND_PY:-$HOME/mission-control-v2/backend/.venv/bin/python}"
|
||||||
|
SMOKE="${SMOKE_SH:-$HOME/mission-control-v2/deploy/self-smoke.sh}"
|
||||||
|
ROT=0
|
||||||
|
|
||||||
|
pruef() { # $1=Nummer $2=Beschreibung $3=Ergebnis(0/1) $4=Detail
|
||||||
|
if [ "$3" -eq 0 ]; then printf ' %s %-34s GRUEN %s\n' "$1" "$2" "${4:-}"
|
||||||
|
else printf ' %s %-34s ROT %s\n' "$1" "$2" "${4:-}"; ROT=1; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -d "$W" ] || { echo "ABBRUCH: Worktree nicht gefunden: $W"; exit 1; }
|
||||||
|
echo "=== Referenz-Check ($W) ==="
|
||||||
|
|
||||||
|
# 1 — Lint
|
||||||
|
if [ -x "$LINT" ]; then
|
||||||
|
out="$("$LINT" check "$W" 2>&1)"; rc=$?
|
||||||
|
pruef 1 "Lint sauber (ruff)" $rc "$(printf '%s' "$out" | tail -1)"
|
||||||
|
else
|
||||||
|
pruef 1 "Lint sauber (ruff)" 1 "ruff fehlt unter $LINT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2 — Backend laedt
|
||||||
|
out="$(cd "$W/backend" && "$PY" -c 'import app' 2>&1)"; rc=$?
|
||||||
|
pruef 2 "Backend importierbar (app)" $rc "$(printf '%s' "$out" | tail -1)"
|
||||||
|
|
||||||
|
# 3 — keine mem0-Reste (docs/ ausgenommen: Historie)
|
||||||
|
n="$(cd "$W" && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' 2>/dev/null | wc -l)"
|
||||||
|
[ "$n" -eq 0 ]; rc=$?
|
||||||
|
pruef 3 "Keine mem0-Reste" $rc "$n Datei(en) mit Treffern"
|
||||||
|
[ "$n" -gt 0 ] && (cd "$W" && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' 2>/dev/null | sed 's/^/ /')
|
||||||
|
|
||||||
|
# 3b — der Sidecar-Ordner selbst
|
||||||
|
if [ -d "$W/mem0_service" ]; then pruef "3b" "Ordner mem0_service entfernt" 1 "liegt noch da"
|
||||||
|
else pruef "3b" "Ordner mem0_service entfernt" 0; fi
|
||||||
|
|
||||||
|
# 4 — Selbsttest (laeuft gegen das LAUFENDE System, nicht gegen den Worktree)
|
||||||
|
if [ -r "$SMOKE" ]; then
|
||||||
|
out="$(timeout 540 bash "$SMOKE" 2>&1)"; rc=$?
|
||||||
|
pruef 4 "Selbsttest self-smoke" $rc "$(printf '%s' "$out" | grep -cE '\bPASS\b') PASS / $(printf '%s' "$out" | grep -cE '\bFAIL\b') FAIL"
|
||||||
|
else
|
||||||
|
pruef 4 "Selbsttest self-smoke" 1 "nicht lesbar: $SMOKE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5 — MC2 lebt
|
||||||
|
curl -s -m 8 localhost:9001/api/health 2>/dev/null | grep -q '"status":"ok"'; rc=$?
|
||||||
|
pruef 5 "MC2 gesund" $rc
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ $ROT -eq 0 ]; then echo " ERGEBNIS: FERTIG — alle Kriterien gruen."
|
||||||
|
else echo " ERGEBNIS: NICHT fertig — mindestens ein Kriterium ist rot."; fi
|
||||||
|
exit $ROT
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Referenzaufgabe: Mem0-Sidecar restlos ausbauen
|
||||||
|
|
||||||
|
> **Zweck dieser Datei:** Messlatte für den Stack-Umbau (Stufe 2, 20.08.2026). Dieselbe Aufgabe wird
|
||||||
|
> mehrfach mit verschiedenen Modellen gefahren. Gemessen wird **nicht** Tokens pro Sekunde, sondern:
|
||||||
|
> *Wurde sie fertig? Wie lange? Wie viele Züge?*
|
||||||
|
> Alle Pfade und Befehle unten sind am 20.08.2026 auf der Box **nachgemessen**, nicht geraten.
|
||||||
|
|
||||||
|
## Hintergrund
|
||||||
|
|
||||||
|
Mem0 wurde im August abgelöst (`bf145a3`, `eb4bcfc`, `2286879`). Der Rückbau blieb unvollständig:
|
||||||
|
Der Sidecar-Ordner liegt noch im Baum, MC2 fragt weiterhin einen toten Port ab.
|
||||||
|
|
||||||
|
Gemessen: `curl http://127.0.0.1:8765/health` → **keine Antwort**. Kein systemd-Dienst dafür,
|
||||||
|
`/srv/models/mem0` bereits entfernt. Es ist also wirklich tot, nicht nur gerade aus.
|
||||||
|
|
||||||
|
## Arbeitsplatz
|
||||||
|
|
||||||
|
**Isolierter Worktree — NICHT das Live-Deployment:**
|
||||||
|
|
||||||
|
```
|
||||||
|
~/projekte/mc2-referenz Zweig: referenz/mem0-ausbau
|
||||||
|
```
|
||||||
|
|
||||||
|
`~/mission-control-v2` bleibt auf `main` und bedient weiter den laufenden Dienst auf `:9001`.
|
||||||
|
**Dort nichts ändern.**
|
||||||
|
|
||||||
|
## ‼️ Arbeitsweise — zuerst lesen
|
||||||
|
|
||||||
|
**Arbeite Datei für Datei. Lies HÖCHSTENS zwei Dateien, bevor du die erste änderst.**
|
||||||
|
|
||||||
|
Nach jeder geänderten Datei gehst du zur nächsten. **Kein Gesamtplan, keine Vollinventur vorab** — du
|
||||||
|
darfst jederzeit nachlesen, wenn du etwas brauchst. Die Liste unten ist eine Landkarte, keine
|
||||||
|
Leseliste.
|
||||||
|
|
||||||
|
*Warum das hier so deutlich steht:* Im ersten Anlauf (20.08., über Nacht) hat der Agent in sechs
|
||||||
|
Sitzungen ~336 Nachrichten und ~360.000 Tokens verbraucht, 19 Dateien gelesen — und **nicht eine
|
||||||
|
einzige Zeile geändert**. Er lief fünfmal in dasselbe Muster: „Ich habe jetzt das vollständige Bild,
|
||||||
|
jetzt lese ich nur noch die restlichen Dateien, *bevor ich anfange*." Dann war das Zug-Limit
|
||||||
|
erreicht. Sein eigenes Fazit: *„Iterationslimit erreicht, bevor ich die erste Zeile geändert habe."*
|
||||||
|
|
||||||
|
Eine halbfertige Änderung ist wertvoller als eine vollständige Analyse ohne Änderung.
|
||||||
|
|
||||||
|
## Auftrag
|
||||||
|
|
||||||
|
Entferne die Mem0-Reste vollständig, ohne etwas anderes kaputtzumachen.
|
||||||
|
|
||||||
|
**17 Dateien haben Treffer** (gemessen 20.08., selbst nachprüfen statt blind vertrauen):
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/steward.py deploy/stack-postcheck.sh
|
||||||
|
backend/config.py deploy/venv-audit.sh
|
||||||
|
backend/routers/voice.py deploy/agent-hooks/box-steckbrief-inject.sh
|
||||||
|
backend/routers/system.py deploy/warmup.sh
|
||||||
|
backend/routers/zeitmaschine.py deploy/autoupdate.sh
|
||||||
|
backend/services/sentry.py deploy/hermes-postcheck.sh
|
||||||
|
backend/services/voice_metrics.py deploy/restore.sh
|
||||||
|
backend/services/backup.py deploy/backup.sh
|
||||||
|
mcp/mcp_mc.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Dazu der Ordner `mem0_service/` (app.py, migrate.py, requirements.txt, smoke_test.py).
|
||||||
|
|
||||||
|
> ‼️ **Der eigentliche Prüfstein:** Wenn eine API-Antwort ein Feld wie `mem0_reachable` liefert, das
|
||||||
|
> das Frontend liest, darf es nicht einfach verschwinden — sonst bricht die Oberfläche. Entweder das
|
||||||
|
> Frontend mit anpassen oder das Feld sauber entfernen. Wer nur `grep mem0` laufen lässt und alles
|
||||||
|
> wegwirft, produziert eine Fassade.
|
||||||
|
|
||||||
|
## Fertig-Kriterien
|
||||||
|
|
||||||
|
Ausgangswert am 20.08.: **1, 2, 4, 5 sind bereits grün** — sie sind Schutzgeländer, nicht die Arbeit.
|
||||||
|
Nur Kriterium 3 ist offen. Wer 3 erfüllt und dabei 1/2/4/5 kaputtmacht, hat versagt.
|
||||||
|
|
||||||
|
| # | Prüfung | Befehl | Ausgangswert |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | Lint sauber | `cd ~/projekte/mc2-referenz && ~/.venv-lint/bin/ruff check .` | ✅ *All checks passed* |
|
||||||
|
| 2 | Backend lädt | `cd ~/projekte/mc2-referenz/backend && ~/mission-control-v2/backend/.venv/bin/python -c "import app"` | ✅ ok |
|
||||||
|
| 3 | **keine mem0-Reste** | `cd ~/projekte/mc2-referenz && grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh' \| wc -l` | ❌ **17** → muss **0** werden |
|
||||||
|
| 4 | Selbsttest grün | `bash ~/mission-control-v2/deploy/self-smoke.sh` | ✅ grün |
|
||||||
|
| 5 | MC2 gesund | `curl -s localhost:9001/api/health` | ✅ `"status":"ok"` |
|
||||||
|
|
||||||
|
Treffer in `docs/` sind erlaubt — das ist Historie und darf stehenbleiben.
|
||||||
|
|
||||||
|
Selbstprüfung mit einem Befehl:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ~/projekte/mc2-referenz/docs/aufgaben/referenz-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> Der Prüfbefehl liegt bewusst unter `docs/aufgaben/` und **nicht** unter `deploy/`. Im ersten Anlauf
|
||||||
|
> lag er in `deploy/`, enthielt selbst siebenmal das Wort „mem0" (er sucht ja danach) und zählte sich
|
||||||
|
> damit in Kriterium 3 mit — das Kriterium war unerfüllbar. In `docs/` wird er nicht durchsucht.
|
||||||
|
|
||||||
|
## Randbedingungen
|
||||||
|
|
||||||
|
- ‼️ **In einer FRISCHEN Sitzung starten**, nicht in einer fortgesetzten. Beim ersten Anlauf hat der
|
||||||
|
Desktop eine Sitzung vom Vortag weitergeführt und deren Kontext mitgeschleppt — die Messung war
|
||||||
|
dadurch wertlos. Im Desktop also einen **neuen Chat** anlegen, nicht den alten öffnen.
|
||||||
|
- **Nicht pushen.** Der Zweig bleibt lokal, damit der Lauf wiederholbar ist.
|
||||||
|
- **Nichts außerhalb des Worktrees anfassen** — kein `/etc`, keine systemd-Dienste, kein Neustart.
|
||||||
|
- Wenn ein Kriterium nicht erfüllbar ist: **ehrlich sagen und aufhören.** Ein klares
|
||||||
|
„Kriterium 3 scheitert an X" ist wertvoller als ein beschönigter Abschluss.
|
||||||
|
|
||||||
|
## Zurücksetzen zwischen den Läufen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/mission-control-v2
|
||||||
|
git worktree remove --force ~/projekte/mc2-referenz
|
||||||
|
git branch -D referenz/mem0-ausbau
|
||||||
|
git worktree add -b referenz/mem0-ausbau ~/projekte/mc2-referenz main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Messprotokoll
|
||||||
|
|
||||||
|
| Lauf | Aufbau | Fertig? | Wanduhr | Züge | Bemerkung |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| — | *Fehlversuch 20.08. 21:10* | ✗ | 76 min, abgebrochen | 8 | Kriterium 3 unerfüllbar + fortgesetzte Alt-Sitzung. **Verwertbar trotzdem:** 19.899 Ausgabe-Tokens, davon nur ~2.600 Zeichen sichtbar → **>95 % unsichtbares Nachdenken**. Kompression feuerte bereits. |
|
||||||
|
| **A** | Qwen3.8-27B, `medium`, `max_turns: 40`, ohne Arbeitsanweisung | ✗ | über Nacht | 6 Sitzungen, ~336 Nachrichten | **0 Dateien geschrieben.** Analyse-Schleife, Zug-Limit erreicht. Werkzeug `file` war die ganze Zeit verfügbar, wurde nie benutzt. ~360k Tokens. Inhaltlich gut: fand `mem0_ms` im Frontend **und** einen toten `memory_svc`-Import in `steward.py`. |
|
||||||
|
| **A2** | dasselbe, **+ Arbeitsanweisung oben**, `max_turns: 80` | | | | ← **als Nächstes.** Prüft, ob die Schleife am Auftrag lag |
|
||||||
|
| D | Qwen3.8-27B, `reasoning_effort: low` | | | | Nächster Hebel, falls A2 auch nur liest |
|
||||||
|
| B | Qwen3.8-27B **+ DFlash-2** | | | | Entwurfsmodell liegt bereit |
|
||||||
|
| C | Qwen3.6-35B-A3B (`fast`, 95,7 t/s) | | | | SWE-bench 73,4 |
|
||||||
|
|
||||||
|
> ★★ **Die Lehre aus Lauf A:** Das ist **kein Tempo-Problem.** Mit 95 t/s statt 12,6 wäre derselbe
|
||||||
|
> Lauf genauso gescheitert — nur schneller. Deshalb kommen A2 und D **vor** den Modellwechseln B und C.
|
||||||
|
|
||||||
|
### Was der Fehlversuch schon gezeigt hat
|
||||||
|
|
||||||
|
Pro Zug erzeugte das Modell ~2.487 Tokens, sichtbar wurden davon 181–451 Zeichen. Bei 12,6 t/s sind
|
||||||
|
das ~26 Minuten reine Erzeugung für ~2.600 Zeichen Text. **Der Engpass ist Denk-Aufwand × Bandbreite,
|
||||||
|
nicht das Harness.** Darum Lauf D vor B und C: er prüft den billigsten Hebel zuerst.
|
||||||
|
|
||||||
|
Inhaltlich war das Modell gut — es fand von selbst den Prüfstein (`mem0_ms` → `memory_ms` im Frontend),
|
||||||
|
statt blind zu löschen.
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
# Umbau: OpenChamber als Coding-Bahn, Lucy bleibt SysAdmin
|
||||||
|
|
||||||
|
_Entschieden 21.08.2026. Rückbau der Hermes-Coding-Bahn ist **erledigt**, der Aufbau von
|
||||||
|
OpenChamber steht aus. Diese Datei ist die Übergabe an die nächste Sitzung._
|
||||||
|
|
||||||
|
## Die Entscheidung
|
||||||
|
|
||||||
|
**Zwei getrennte Bahnen** — der Weg, der bei diesem Stack schon einmal funktioniert hat:
|
||||||
|
|
||||||
|
| Bahn | Werkzeug | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| **Betrieb** | Hermes-Runtime (Lucy) | SysAdmin, Telegram, Crons, Gedächtnis, Skills, Stimme, PC-Steuerung |
|
||||||
|
| **Coding** | **OpenChamber** über OpenCode | Nur programmieren. Getrennt von Lucy. |
|
||||||
|
| **Motor** | llama-swap `:8080` / MC2-Gateway `:9010` | beide Bahnen teilen sich die Modelle |
|
||||||
|
| **Steuerpult** | MC2 `:9001` | unverändert |
|
||||||
|
|
||||||
|
**Warum getrennt:** Der Versuch, beides in Hermes Desktop zu vereinen, ist an zwei Dingen
|
||||||
|
gescheitert — die Gateway-Registry akzeptiert kein Benutzer/Passwort (nur Session-Token oder
|
||||||
|
OAuth, beides für eine selbstgehostete Box ungeeignet), und der Sessions-Reiter fiel dadurch
|
||||||
|
immer auf „This device" zurück. Details in der Gedächtnisnotiz vom 20./21.08.
|
||||||
|
|
||||||
|
## Was bereits erledigt ist (21.08.)
|
||||||
|
|
||||||
|
**PC:**
|
||||||
|
- Hermes Desktop + CLI deinstalliert (`hermes uninstall --full`), Reste von Hand entfernt
|
||||||
|
- `AppData\Local\hermes`, `AppData\Roaming\Hermes`, `~\.hermes` — alle weg
|
||||||
|
- Startmenü- und Desktop-Verknüpfungen entfernt
|
||||||
|
- Env-Variablen bereinigt; **`HERMES_PC_TOKEN` bleibt bewusst** (Lucys PC-Executor auf `:7777`)
|
||||||
|
- Node v24.19.0 in `Program Files` ist unabhängig → bleibt, erfüllt OpenChambers Anforderung (≥22)
|
||||||
|
|
||||||
|
**Box:**
|
||||||
|
- Bot-Profile `coder` + `debugger` entfernt (gesichert in `~/archiv-aufraeumen-20260820/`)
|
||||||
|
- `~/.hermes/desktop-ssh/` entfernt, `desktop-auth.env` entfernt
|
||||||
|
- ufw: Port **9119 wieder zu** — nur noch `22 · 9001 · 8080 · 7681`
|
||||||
|
- Dashboard bleibt auf `0.0.0.0:9119` gebunden **mit Anmeldung**, damit MC2s `/hermes-ui`-Proxy
|
||||||
|
nicht mehr passwortlos aus dem LAN erreichbar ist (das war ein offenes Scheunentor).
|
||||||
|
Zugang: `~/.hermes/dashboard-login.txt` (0600), Benutzer `commander`.
|
||||||
|
‼️ Zugangsdaten stehen **nur** in `config.yaml` — die frühere `EnvironmentFile` hat sie still
|
||||||
|
überstimmt und kostete eine Fehlersuche. Eine Quelle, eine Wahrheit.
|
||||||
|
|
||||||
|
**Verifiziert nach dem Rückbau:** 5 Dienste aktiv · 4 MCP-Server · 4 Crons · Telegram `connected`
|
||||||
|
· PC-Executor HTTP 200. Lucy ist vollständig funktionsfähig.
|
||||||
|
|
||||||
|
## Was aufzubauen ist
|
||||||
|
|
||||||
|
### 1. OpenCode auf der Box
|
||||||
|
|
||||||
|
Wurde beim Juli-Rückbau entfernt (`command -v opencode` → leer). Muss neu.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Installation (Box)
|
||||||
|
curl -fsSL https://opencode.ai/install | bash
|
||||||
|
|
||||||
|
# Provider auf den lokalen Motor zeigen lassen: ~/.config/opencode/opencode.json
|
||||||
|
# base_url http://127.0.0.1:9010/v1 (MC2-Gateway, kann model:auto)
|
||||||
|
# oder http://127.0.0.1:8080/v1 (llama-swap direkt)
|
||||||
|
# api_key local
|
||||||
|
```
|
||||||
|
|
||||||
|
Als systemd-**User**-Dienst (wie alle anderen — reboot-fest, sudo-frei):
|
||||||
|
|
||||||
|
```
|
||||||
|
opencode serve --hostname 0.0.0.0 --port 4096
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach `ufw allow from 192.168.178.0/24 to any port 4096 proto tcp`.
|
||||||
|
|
||||||
|
### 2. OpenChamber auf dem PC
|
||||||
|
|
||||||
|
Desktop-Version von den GitHub-Releases (`openchamber/openchamber`, MIT, ~9,1k ★).
|
||||||
|
**Wichtig:** Die Desktop-Variante bringt eine eigene OpenCode-CLI mit — für den Remote-Betrieb
|
||||||
|
muss sie ausgeschaltet werden:
|
||||||
|
|
||||||
|
```
|
||||||
|
OPENCODE_HOST = http://192.168.178.151:4096 ← volle Adresse MIT Port, OHNE Pfad
|
||||||
|
OPENCODE_SKIP_START = true
|
||||||
|
```
|
||||||
|
|
||||||
|
> Fehlt der Port oder hängt ein Pfad dran, **ignoriert OpenChamber die Variable und startet
|
||||||
|
> stillschweigend seinen eigenen Server.** Genau diese Klasse Fehler hat uns bei Hermes einen
|
||||||
|
> halben Tag gekostet.
|
||||||
|
|
||||||
|
Modelle, Provider und Schlüssel liegen in **OpenCode**, nicht in OpenChamber.
|
||||||
|
|
||||||
|
### 3. Modellwahl — nicht wieder den dichten Coder
|
||||||
|
|
||||||
|
★★ **`fast` (Qwen3.6-35B-A3B) nehmen, nicht `coder` (Qwen3.8-27B).** Gemessen:
|
||||||
|
|
||||||
|
| | Ausgabe | Eingabe | SWE-bench Verified |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `fast` Qwen3.6-35B-A3B (MoE) | **95,7 t/s** | 221 t/s | **73,4** |
|
||||||
|
| `coder` Qwen3.8-27B (dicht) | 12,6 t/s | 144 t/s | keine agentische Messung |
|
||||||
|
|
||||||
|
Das dichte Modell ist bandbreitenlimitiert (17 GB ÷ 215 GB/s ≈ 12,6 t/s = Hardware-Limit).
|
||||||
|
Der MoE ist **schneller und besser**.
|
||||||
|
|
||||||
|
### 4. Die Arbeitsanweisung mitnehmen — der wichtigste Fund überhaupt
|
||||||
|
|
||||||
|
Der Agent las in Lauf A eine ganze Nacht lang und schrieb **null Dateien**. Mit dieser Zeile
|
||||||
|
im Auftrag schrieb er in 24 Minuten fünf:
|
||||||
|
|
||||||
|
> **„Arbeite Datei für Datei. Lies HÖCHSTENS zwei Dateien, bevor du die erste änderst.
|
||||||
|
> Kein Gesamtplan, keine Vollinventur. Eine halbfertige Änderung ist wertvoller als eine
|
||||||
|
> vollständige Analyse ohne Änderung."**
|
||||||
|
|
||||||
|
Gehört in OpenChambers System-Prompt bzw. `AGENTS.md` der Projekte.
|
||||||
|
|
||||||
|
## Bekannte Fallen (aus dem Juli-Audit + heute)
|
||||||
|
|
||||||
|
- **Bus-Faktor 1** bei OpenChamber: ~70–80 % der Commits von einer Person. Version **pinnen**,
|
||||||
|
nicht blind auto-updaten.
|
||||||
|
- **Windows-Installer unsigniert** → SmartScreen-Warnung ist erwartbar, kein Alarm.
|
||||||
|
- Nie ohne `--ui-password` über localhost hinaus binden.
|
||||||
|
- `opencode web` (nicht OpenChamber!) hat einen kaputten Projekt-Finder für `$HOME` — falls
|
||||||
|
irgendwo ein leerer Projektbaum auftaucht, ist das der Grund.
|
||||||
|
- Git-Push zum Gitea geht vom PC **nur über PowerShell/GCM**, nicht aus der Git-Bash.
|
||||||
|
|
||||||
|
## Erster Test nach dem Aufbau
|
||||||
|
|
||||||
|
Die Referenzaufgabe liegt bereit und ist halb erledigt — ideal zum Vergleich:
|
||||||
|
|
||||||
|
```
|
||||||
|
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
||||||
|
Aufgabe: docs/aufgaben/referenzaufgabe-mem0-ausbau.md
|
||||||
|
Prüfung: bash docs/aufgaben/referenz-check.sh
|
||||||
|
Stand: 5 von 17 Dateien erledigt (Hermes-Lauf A2, abgebrochen durch SSH-Fehler)
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit lässt sich direkt vergleichen: **Wie lange braucht OpenChamber + `fast` für dieselben
|
||||||
|
17 Dateien?** Hermes + dichter Coder brauchte hochgerechnet ~80 Minuten; die Rechnung für
|
||||||
|
`fast` sagt ~30.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stand nach dem Aufbau (21.08.2026)
|
||||||
|
|
||||||
|
**Beide Bahnen stehen. Der Aufbau ist erledigt, der Referenzlauf steht aus.**
|
||||||
|
|
||||||
|
### Box (`192.168.178.151`)
|
||||||
|
|
||||||
|
| Sache | Wert |
|
||||||
|
|---|---|
|
||||||
|
| OpenCode | **1.18.20**, `~/.opencode/bin/opencode` (eigenständiges Binary, kein Node nötig) |
|
||||||
|
| Dienst | `opencode-server.service` (systemd **user**, `enable`d, Linger=yes → reboot-fest) |
|
||||||
|
| Lauscht | `0.0.0.0:4096`, Arbeitsverzeichnis `~/projekte` |
|
||||||
|
| ufw | Regel 5: `4096/tcp ALLOW IN 192.168.178.0/24` |
|
||||||
|
| Provider | `box` → `http://127.0.0.1:9010/v1` (MC2-Gateway), `apiKey: local` |
|
||||||
|
| Modelle | `box/fast` (Standard) · `box/heavy` · `box/hermes` (small_model) · `box/coder` (nur Vergleich) |
|
||||||
|
| Autoupdate | `false` in `opencode.json` |
|
||||||
|
|
||||||
|
Konfiguration: `~/.config/opencode/opencode.json`.
|
||||||
|
**Nur Rollen-Aliase**, keine Modell-Eigennamen — die Konsolidierung wirft Eigennamen sonst raus (Regel vom 20.08.).
|
||||||
|
|
||||||
|
### Arbeitsanweisung — global verdrahtet
|
||||||
|
|
||||||
|
Liegt als `~/.config/opencode/ARBEITSANWEISUNG.md` und ist über `instructions` in
|
||||||
|
`opencode.json` in **jeder** Sitzung aktiv, nicht nur in Projekten mit `AGENTS.md`.
|
||||||
|
Gegengeprüft: „Wie viele Dateien höchstens?" → `2`. `~` wird von OpenCode expandiert.
|
||||||
|
|
||||||
|
### PC
|
||||||
|
|
||||||
|
| Sache | Wert |
|
||||||
|
|---|---|
|
||||||
|
| OpenChamber | **v1.19.0** (18.08.), `%LOCALAPPDATA%\Programs\@openchamberelectron` |
|
||||||
|
| Signatur | `NotSigned` — erwartet, kein Alarm |
|
||||||
|
| `OPENCODE_HOST` | `http://192.168.178.151:4096` (User-Env, mit Port, ohne Pfad) |
|
||||||
|
| `OPENCODE_SKIP_START` | `true` (User-Env) |
|
||||||
|
|
||||||
|
**Verifiziert:** OpenChamber hält 8 offene TCP-Verbindungen zur Box auf `:4096`,
|
||||||
|
startet **keinen** eigenen OpenCode (kein lokaler Prozess, kein lokaler Port).
|
||||||
|
Damit ist genau der Punkt genommen, an dem Hermes Desktop gescheitert ist.
|
||||||
|
|
||||||
|
**Auto-Update:** kein Riegel nötig. Im Bundle steht `autoUpdater.autoDownload = false` und
|
||||||
|
`autoInstallOnAppQuit = false` — OpenChamber lädt nie von selbst, es meldet nur und wartet
|
||||||
|
auf einen Klick. Die Version ist damit faktisch gepinnt.
|
||||||
|
|
||||||
|
### ‼️ Offene Sicherheitsentscheidung — bewusst so gewählt
|
||||||
|
|
||||||
|
Der Server läuft **ohne Anmeldung** im LAN. Beim Start meldet er selbst:
|
||||||
|
|
||||||
|
```
|
||||||
|
Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.
|
||||||
|
```
|
||||||
|
|
||||||
|
Praktisch heißt das: jedes Gerät im `192.168.178.0/24` kann über die API beliebige
|
||||||
|
Befehle als `hitonabi` auf der Box ausführen — dieselbe Klasse Loch wie das passwortlose
|
||||||
|
Dashboard auf `:9119`, das am 20.08. zugemacht wurde.
|
||||||
|
|
||||||
|
★★ **Ein Riegel wäre da und kostet nichts:** OpenChamber schickt HTTP-Basic-Auth mit, wenn
|
||||||
|
`OPENCODE_SERVER_PASSWORD` gesetzt ist (Benutzer aus `OPENCODE_SERVER_USERNAME`,
|
||||||
|
Standard `opencode`). Server-Variable in der systemd-Unit, gleiche Variable als User-Env
|
||||||
|
auf dem PC — **kein Tunnel, keine Umstellung, der Port bleibt offen im LAN.**
|
||||||
|
|
||||||
|
Der User hat am 21.08. **bewusst dagegen entschieden** („so lassen"). Nicht neu aufrollen,
|
||||||
|
aber hier notiert, damit es kein stiller Defekt bleibt.
|
||||||
|
|
||||||
|
### Nächster Schritt: der Referenzlauf
|
||||||
|
|
||||||
|
Alles steht bereit, nichts wurde am Prüfstand angefasst:
|
||||||
|
|
||||||
|
```
|
||||||
|
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
||||||
|
Stand: 5 von 17 Dateien erledigt, referenz-check.sh sagt ROT
|
||||||
|
Prüfung: bash docs/aufgaben/referenz-check.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Arbeitsanweisung wirkt dort über die **globale** Instruktion — `AGENTS.md` im Worktree
|
||||||
|
wurde absichtlich **nicht** angefasst, damit der Vergleich zu Lauf A/A2 sauber bleibt.
|
||||||
|
Zu messen: **Wie lange braucht OpenChamber + `fast` für dieselben 17 Dateien?**
|
||||||
|
Hermes + dichter `coder` brauchte hochgerechnet ~80 Minuten; die Rechnung für `fast` sagt ~30.
|
||||||
|
|
||||||
|
### ‼️ Falle: der Ordner-Knopf reicht Windows-Pfade an die Box durch
|
||||||
|
|
||||||
|
**Beim ersten Lauf sofort hineingetappt (21.08., ~1 h verloren).** Die Sitzung stand auf:
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/hitonabi/projekte/F:\Coding Stuff\mission-control-2
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenChamber hatte den **Windows-Pfad des PCs** an den Box-Server durchgereicht, der ihn hinten
|
||||||
|
an sein `WorkingDirectory` klebte. Das Verzeichnis existiert auf der Box nicht → der Agent hat
|
||||||
|
kein Arbeitsverzeichnis, es geht **keine einzige Anfrage** an den Motor raus. In der Oberfläche
|
||||||
|
sieht das aus wie „hängt": Nachricht steht da, eine Zusammenfassung erscheint, danach nichts.
|
||||||
|
`tokens: 0/0`.
|
||||||
|
|
||||||
|
**Ursache, im Bundle nachgesehen:** OpenChamber öffnet Ordner über Electrons natives
|
||||||
|
`showOpenDialog` — den **lokalen** Windows-Dateibrowser. Einen Fern-Auswähler gibt es nicht.
|
||||||
|
Im Fernbetrieb liefert dieser Knopf also *immer* einen unbrauchbaren Pfad.
|
||||||
|
|
||||||
|
**Der richtige Weg:** Seitenleiste → **„Projekt hinzufügen"**. Das ist ein eigener Dialog
|
||||||
|
(`directoryExplorerDialog`) mit Baum **und** Eingabefeld („Enter path or select from tree…"),
|
||||||
|
und der geht über den Server — also über die Box.
|
||||||
|
|
||||||
|
**Diagnose in einem Befehl** — wenn wieder „nichts passiert", zeigt das den Grund sofort:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh hitonabi@192.168.178.151 "curl -s http://127.0.0.1:4096/session | python3 -c \"import json,sys; s=json.load(sys.stdin); s.sort(key=lambda x: x['time']['updated'], reverse=True); x=s[0]; print(x['directory'], x.get('model'), x.get('tokens'))\""
|
||||||
|
```
|
||||||
|
|
||||||
|
Steht dort ein Pfad mit `F:\` oder `tokens 0/0`, ist es diese Falle.
|
||||||
|
|
||||||
|
**Merke:** Ein Projekt taucht in der Server-Liste erst auf, wenn dort einmal eine Sitzung lief.
|
||||||
|
`mc2-referenz` wurde am 21.08. mit einem harmlosen Lauf angemeldet (`box/hermes`, „Antworte nur
|
||||||
|
mit OK") — Worktree danach nachgemessen unverändert: 5 geänderte Dateien, HEAD `e04ace2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ▶▶ Kurswechsel 21.08. (nachmittags): Quelle ist der PC, nicht die Box
|
||||||
|
|
||||||
|
Der Fernbetrieb oben war **falsch herum gedacht**. Der User arbeitet so — und so ist es
|
||||||
|
jetzt gebaut:
|
||||||
|
|
||||||
|
```
|
||||||
|
F:\Coding Stuff\… ← Quelle der Wahrheit, liegt IMMER lokal
|
||||||
|
│ OpenChamber startet seine EIGENE OpenCode-CLI und arbeitet an lokalen Dateien
|
||||||
|
│ git push (PowerShell/GCM)
|
||||||
|
▼
|
||||||
|
Gitea 192.168.178.153:3000
|
||||||
|
│ projekte-sync (stuendlich, --ff-only) — oder sofort per Skript
|
||||||
|
▼
|
||||||
|
AI-Box ~/projekte/… ← Spiegel fuer Deploy / CI / Lucy
|
||||||
|
▲
|
||||||
|
└─ von der Box kommt NUR noch das Modell (Inferenz ueber HTTP)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Damit faellt die Windows-Pfad-Falle ersatzlos weg** — es gibt keine Fernpfade mehr.
|
||||||
|
|
||||||
|
### ★★ Kein Box-Umbau noetig: MC2 `:9001/v1` ist der Modellweg
|
||||||
|
|
||||||
|
Erst war geplant, den Gateway `:9010` ins LAN zu binden. Ueberfluessig — die Gateway-Unit
|
||||||
|
sagt es selbst: *„LAN-Clients kommen weiter ueber MC2 `:9001/v1`, das roh hierher
|
||||||
|
durchreicht."* Der Port ist seit jeher offen.
|
||||||
|
|
||||||
|
Vom PC aus gemessen: `:9001/v1/models` liefert **die Rollen-Aliase** (`fast`, `heavy`,
|
||||||
|
`hermes`, `coder`), `fast` antwortet in **0,7 s**. Kein neuer Port, keine Neubindung,
|
||||||
|
keine ufw-Regel — und **keine Modell-Eigennamen** in der PC-Konfiguration (die Regel vom
|
||||||
|
20.08. bleibt gewahrt). llama-swap `:8080` waere die Alternative gewesen, liefert aber nur
|
||||||
|
Eigennamen (`Qwen3.6-35B-A3B`) → genau der stille 404 vom 19.08.
|
||||||
|
|
||||||
|
### Was auf dem PC steht
|
||||||
|
|
||||||
|
`C:\Users\TobisPC\.config\opencode\opencode.json` — Provider `aibox` →
|
||||||
|
`http://192.168.178.151:9001/v1`, Standard `aibox/fast`, `small_model` `aibox/hermes`.
|
||||||
|
Die Juli-Struktur ist erhalten: Agenten `plan`(heavy) · `build`(fast) · `explore`(hermes) ·
|
||||||
|
`review`(heavy), inklusive **IDE-Zaun** (`ssh`/`scp`/`sftp` deny, gezielte Arcane-Ausnahme;
|
||||||
|
Catch-all `*: allow` steht ZUERST, weil OpenCode `findLast` auswertet).
|
||||||
|
Arbeitsanweisung als `ARBEITSANWEISUNG.md` daneben, ueber `instructions` global geladen.
|
||||||
|
|
||||||
|
Das Plugin `plugin/mc2-governor.ts` wird automatisch mitgeladen: Werkzeug-Zaun,
|
||||||
|
Pruef-Tor-Schleife, Savepoint statt Zusammenfassen, Meldungen an Lucys Stimme.
|
||||||
|
‼️ Es blockt **`git push`** absichtlich — *„Veroeffentlichen ist Sache des Menschen."*
|
||||||
|
Der Agent codet, gepusht wird von Hand (siehe Skript unten).
|
||||||
|
|
||||||
|
**Gebuendelte CLI:** OpenCode **1.18.18** in OpenChambers `resources\opencode-cli`.
|
||||||
|
Im lokalen Betrieb vergibt OpenChamber dem Server automatisch ein Passwort
|
||||||
|
(`OPENCODE_SERVER_PASSWORD`, lifecycle-owned) — `/config` antwortet von aussen mit `401`.
|
||||||
|
Der Schutz, den der Box-Server nicht hatte, ist hier also gratis dabei.
|
||||||
|
|
||||||
|
**Env-Variablen `OPENCODE_HOST` und `OPENCODE_SKIP_START` sind wieder ENTFERNT.**
|
||||||
|
Sie erzwingen den Fernbetrieb; mit ihnen startet OpenChamber keine eigene CLI.
|
||||||
|
|
||||||
|
### Box zurueckgebaut
|
||||||
|
|
||||||
|
`opencode-server.service` ist **gestoppt, deaktiviert und geloescht**, die ufw-Regel fuer
|
||||||
|
`4096` wieder entfernt. Damit erledigt sich die offene Sicherheitsfrage von heute Mittag
|
||||||
|
von selbst: der ungeschuetzte Dienst existiert nicht mehr. ufw steht wieder auf
|
||||||
|
`22 · 9001 · 8080 · 7681`.
|
||||||
|
|
||||||
|
### `deploy/push-und-sync.ps1` — der Handschlag
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\deploy\push-und-sync.ps1 # committen -> pushen -> Box zieht sofort nach
|
||||||
|
.\deploy\push-und-sync.ps1 -NurSync # nur nachziehen
|
||||||
|
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Stoesst `projekte-sync` sofort an, statt bis zu 60 Minuten auf den Timer zu warten —
|
||||||
|
**kein zweiter Mechanismus, nur ein Ausloeser.** Meldet danach, auf welchem Commit die Box steht.
|
||||||
|
|
||||||
|
‼️ **Falle beim Bauen gefunden:** Der Repo-Name darf **nicht** aus dem Ordnernamen kommen —
|
||||||
|
lokal heisst es `mission-control-2`, in Gitea `mission-control-v2`. Das Skript liest ihn
|
||||||
|
aus der Remote-URL. Getestet gegen beide Faelle: MC2 (Live-Deployment, liegt als
|
||||||
|
Verknuepfung unter `~/projekte`) und `rippy` (normales Projekt, `a14449e`).
|
||||||
|
|
||||||
|
### Offener Punkt: PC-Remote haengt an der DDNS-Domain
|
||||||
|
|
||||||
|
Der PC pusht nach `https://git.tobisniceshomelab.ddnsfree.com/…`, die Box nutzt intern
|
||||||
|
`http://192.168.178.153:3000`. Die Domain ist nachts durch die Zwangstrennung zeitweise
|
||||||
|
tot (Lehre vom 24.07.). Umstellen mit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote set-url origin http://192.168.178.153:3000/Hitonabi/mission-control-v2.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Noch **nicht** gemacht — aendert Git-Konfiguration in den Repos des Users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abschluss der Coding Lane (21.08.2026, abends)
|
||||||
|
|
||||||
|
### Der Zugang: ein SSH-Schluessel, kein Token mehr
|
||||||
|
|
||||||
|
**Der Ausloeser:** Mitten in der Sitzung scheiterte ein Push, der eine Stunde
|
||||||
|
vorher noch ging — `remote: Failed to authenticate user`. Der Git Credential
|
||||||
|
Manager haelt fuer Gitea ein **OAuth-Token mit einer Stunde Laufzeit**. Damit ist
|
||||||
|
HTTP fuer einen Agenten, der selbst pushen soll, strukturell ungeeignet.
|
||||||
|
|
||||||
|
**Was dabei herauskam:** Giteas SSH war **nie funktionsfaehig**. In der `app.ini`
|
||||||
|
stand zwar `DISABLE_SSH = false` und `SSH_PORT = 22`, aber im Container gibt es
|
||||||
|
**keinen `git`-Benutzer** und keine `authorized_keys` — Port 22 gehoert dem
|
||||||
|
System-sshd. Es sah nur so aus, als koennte man ueber SSH klonen.
|
||||||
|
|
||||||
|
**Loesung:** Giteas **eingebauten** SSH-Server eingeschaltet (Port 22 ist belegt,
|
||||||
|
deshalb 2222). Der verwaltet die Schluessel selbst, direkt aus der Datenbank —
|
||||||
|
kein `git`-Benutzer, keine `authorized_keys`.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# /etc/gitea/app.ini, [server] (Sicherung: app.ini.bak-20260821)
|
||||||
|
SSH_PORT = 2222
|
||||||
|
SSH_LISTEN_PORT = 2222
|
||||||
|
START_SSH_SERVER = true
|
||||||
|
SSH_LISTEN_HOST = 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
‼️‼️ **Der SSH-Benutzer heisst `gitea`, NICHT `git`.** Gitea laeuft unter diesem
|
||||||
|
Namen und weist alles andere ab. Die Fehlermeldung steht nur im Gitea-Log, nicht
|
||||||
|
beim Client — ssh sagt bloss `Permission denied (publickey)`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Invalid SSH username git - must use gitea for all git operations via ssh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auf dem PC** (`~/.ssh/config`) — noetig, weil hier **acht** Schluessel liegen
|
||||||
|
und ssh sonst der Reihe nach durchprobiert, bis Gitea abbricht:
|
||||||
|
|
||||||
|
```
|
||||||
|
Host 192.168.178.153 gitea gitea.heimnetz
|
||||||
|
HostName 192.168.178.153
|
||||||
|
Port 2222
|
||||||
|
User gitea
|
||||||
|
IdentityFile ~/.ssh/id_general
|
||||||
|
IdentitiesOnly yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Schluessel `id_general` ist in Gitea hinterlegt (Nr. 3) — **derselbe, mit dem
|
||||||
|
du auf Box, pve und Arcane kommst.** Von acht Zugangsdaten auf einen.
|
||||||
|
|
||||||
|
Alle sieben PC-Repos stehen jetzt auf
|
||||||
|
`ssh://gitea@192.168.178.153:2222/Hitonabi/<repo>.git`.
|
||||||
|
Das TTT2-Projekt bleibt unberuehrt — es haengt an einer anderen Domain und ist
|
||||||
|
als „Finished" markiert.
|
||||||
|
|
||||||
|
### Gemessen, nicht angenommen
|
||||||
|
|
||||||
|
| Test | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| `ssh -T gitea` | „Hi there, Hitonabi! You've successfully authenticated with the key named id_general" |
|
||||||
|
| Push aus PowerShell | 8 Commits, `7c23ebf..25dbfb2` |
|
||||||
|
| **Push aus dem Agenten** | durchgelaufen, **keine Passwortabfrage** |
|
||||||
|
| Erzwungener Push aus dem Agenten | vom Werkzeug-Zaun abgewiesen |
|
||||||
|
| `push-und-sync.ps1` komplett | Commit → Push → Box zieht nach |
|
||||||
|
|
||||||
|
### Der Sonderfall MC2
|
||||||
|
|
||||||
|
`projekte-sync` zieht `mission-control-v2` **absichtlich nicht** — die Box
|
||||||
|
bedient daraus den laufenden Dienst auf `:9001`. Nach einem Push steht dort
|
||||||
|
weiter der alte Commit, und das ist richtig so. Das Live-Deployment braucht
|
||||||
|
seinen eigenen, bewussten Schritt. Alle anderen Projekte werden normal gezogen.
|
||||||
|
|
||||||
|
### Fallen fuer das naechste Mal
|
||||||
|
|
||||||
|
- **`sed` mit `$` und `\n` ueber PowerShell zerlegt sich.** Der erste Anlauf,
|
||||||
|
die `app.ini` zu aendern, lief ins Leere und die Datei blieb unveraendert —
|
||||||
|
ohne Fehlermeldung. In einzelne Ersetzungen zerlegen und **danach nachsehen**.
|
||||||
|
- Ein Dienst-Neustart nach einer Konfig-Aenderung ist noch keine Bestaetigung:
|
||||||
|
Gitea startete brav neu — mit der alten Datei.
|
||||||
|
- Acht SSH-Schluessel auf dem PC sind selbst ein Befund. `IdentitiesOnly yes`
|
||||||
|
ist Pflicht, sonst bricht die Gegenseite vorher ab.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Hermes: Werkzeuge nach Bedarf (21.08.2026)
|
||||||
|
|
||||||
|
## Die Frage war: kann Hermes Werkzeuge je nach Aufgabe an- und abschalten?
|
||||||
|
|
||||||
|
**Ja — auf drei Ebenen, und die wichtigste lief schon.**
|
||||||
|
|
||||||
|
### 1. Automatisch zur Laufzeit — `tool_search` (war bereits an)
|
||||||
|
|
||||||
|
```
|
||||||
|
tools:
|
||||||
|
tool_search:
|
||||||
|
enabled: true
|
||||||
|
threshold_pct: 10
|
||||||
|
listing: auto
|
||||||
|
listing_max_tokens: 4000
|
||||||
|
```
|
||||||
|
|
||||||
|
Im Log nachweisbar:
|
||||||
|
|
||||||
|
```
|
||||||
|
tool_search activated (tier 1): 47 core/visible tools kept,
|
||||||
|
59 deferred (~5636 tokens), listing full (budget ~4000 tokens)
|
||||||
|
```
|
||||||
|
|
||||||
|
Hermes haelt einen Kern sichtbar und schiebt den Rest hinter eine Suche; der Agent
|
||||||
|
zieht nach, was er braucht. **Hier war nichts einzurichten.**
|
||||||
|
|
||||||
|
Aber: das Aufschieben allein sparte nur 1.100–5.600 Tokens, waehrend `kanban` mit
|
||||||
|
22,7 KB im sichtbaren Kern blieb. Der Hebel lag woanders.
|
||||||
|
|
||||||
|
### 2. Global abschalten — `agent.disabled_toolsets`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes config set agent.disabled_toolsets "[computer_use, image_gen, video, video_gen, kanban, delegation]"
|
||||||
|
hermes tools disable session_search
|
||||||
|
```
|
||||||
|
|
||||||
|
Abgeschaltet **anhand von 9 Tagen echter Nutzung**, nicht nach Gefuehl:
|
||||||
|
|
||||||
|
| Werkzeugsatz | Groesse | Nutzung 13.–21.08. |
|
||||||
|
|---|---|---|
|
||||||
|
| `kanban` (14 Werkzeuge) | 22,7 KB | 35× lesen, **1× schreiben** |
|
||||||
|
| `session_search` | 6,8 KB | **1×** |
|
||||||
|
| `delegation` | 5,6 KB | Fan-out, seit OpenChamber tot |
|
||||||
|
|
||||||
|
‼️ `kanban` kennt `hermes tools disable` **nicht** — es haengt an `toolsets` /
|
||||||
|
`platform_toolsets` und muss ueber `agent.disabled_toolsets` weg.
|
||||||
|
|
||||||
|
### 3. Pro Zugangsweg — `platform_toolsets.<platform>`
|
||||||
|
|
||||||
|
★★ **Der groesste Fund.** Es gibt einen eigenen Zugangsweg `cron` (17 Sitzungen im
|
||||||
|
Log), fuer den **kein** Eintrag existierte — Cron-Jobs bekamen also die volle
|
||||||
|
Werkzeugkiste. Von den drei Jobs braucht nur der Nachrichtenbericht ueberhaupt
|
||||||
|
Werkzeuge, und der braucht zwei:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hermes config set platform_toolsets.cron "[web, terminal]"
|
||||||
|
```
|
||||||
|
|
||||||
|
`web` fuer die Suche, `terminal` um `notify.sh` aufzurufen. Mehr nicht.
|
||||||
|
|
||||||
|
Ausserdem moeglich, hier nicht gebraucht: `hermes -t TOOLSETS` setzt den
|
||||||
|
Werkzeugsatz fuer einen einzelnen Aufruf.
|
||||||
|
|
||||||
|
## Gemessen mit `hermes prompt-size`
|
||||||
|
|
||||||
|
Vorlast **je Anfrage**, Systemprompt plus Werkzeug-Schemata:
|
||||||
|
|
||||||
|
| Zugangsweg | vorher | nachher | |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cli` | 99,4 KB | **56,5 KB** | −43 % |
|
||||||
|
| `cron` | 63,1 KB | **16,4 KB** | **−74 %** |
|
||||||
|
|
||||||
|
Rund 11.000 Tokens weniger bei jedem interaktiven Zug, rund 12.000 bei jedem
|
||||||
|
Cron-Lauf. Das ist Kontext, der vorher fuer Werkzeugbeschreibungen draufging,
|
||||||
|
die nie aufgerufen wurden.
|
||||||
|
|
||||||
|
## Nachgeprueft, nicht angenommen
|
||||||
|
|
||||||
|
- Lucy interaktiv: Terminal, Dateien, Websuche, Gedaechtnis — alle vier da.
|
||||||
|
- Daily News Report mit nur `[web, terminal]` durchgelaufen und zugestellt.
|
||||||
|
Der Bericht war sogar besser als der Lauf davor.
|
||||||
|
- Sicherung der Konfiguration liegt unter `~/.hermes/config.yaml.bak-kiss-20260821`.
|
||||||
|
|
||||||
|
## Noch drin, wenn es noch schlanker soll
|
||||||
|
|
||||||
|
- `api_server` traegt 44,8 KB (19 Werkzeuge) — mehr als `cli`. Dort haengt noch
|
||||||
|
`hermes-pc-control`, das in 9 Tagen **null Mal** aufgerufen wurde und an dem
|
||||||
|
eine geplante Aufgabe auf dem PC plus ein eigenes Token haengen.
|
||||||
|
- MCP `hermes-web-fetch` (1 Aufruf) doppelt das eingebaute `web_extract`.
|
||||||
|
- MCP `mission-control-voice` (1 Aufruf) doppelt das eingebaute `text_to_speech`.
|
||||||
|
- Die alten Eintraege `kanban` / `delegation` stehen noch in `platform_toolsets`;
|
||||||
|
`agent.disabled_toolsets` sticht sie, aber aufgeraeumt ist es nicht.
|
||||||
+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` | gpt-oss-120b (mxfp4) | 600 | 32768 ctx; Chef-Gutachter (nur nachts / gezielter On-Demand-Call) |
|
||||||
| `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)
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user