Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b639870ff | ||
|
|
084dd73f71 | ||
|
|
84b4643f1a | ||
|
|
41f2566b29 | ||
|
|
30ab2c013a | ||
|
|
d508a848f5 | ||
|
|
e04ace242c | ||
|
|
d880a76b6a | ||
|
|
ff319ff291 | ||
|
|
6e3440379b | ||
|
|
5f35f0d580 | ||
|
|
43fa747bc3 | ||
|
|
83c6ecc613 | ||
|
|
b316ad40ba | ||
|
|
8c371b946b | ||
|
|
1a2051e988 | ||
|
|
5d93822a7e | ||
|
|
22868795c9 | ||
|
|
9a35be91f9 | ||
|
|
241b1d54c6 | ||
|
|
eb4bcfc4b3 | ||
|
|
d2a175b0ad | ||
|
|
bf145a3f97 | ||
|
|
5b0909089f | ||
|
|
4b6c6ebfd5 | ||
|
|
0a6b009cb6 | ||
|
|
adc73c817f | ||
|
|
ff795ba026 | ||
|
|
46edbe50c1 | ||
|
|
cd7e1bc114 | ||
|
|
600d0caad6 | ||
|
|
733582900a | ||
|
|
8b14dfdc14 | ||
|
|
dced44548d | ||
|
|
4c0af92b3c | ||
|
|
5cc1e6bea1 | ||
|
|
102149a5d6 | ||
|
|
0c849f4dcd | ||
|
|
94490f114d | ||
|
|
c360bbe14e | ||
|
|
6cb29ee70e | ||
|
|
a6e2f78c27 | ||
|
|
a201c20bf2 | ||
|
|
56676fe83c | ||
|
|
77bead5ba4 | ||
|
|
ee456de713 | ||
|
|
e16f0140f7 | ||
|
|
637c21387a | ||
|
|
da03de18cb | ||
|
|
1b0184eed8 | ||
|
|
4fb8387f79 | ||
|
|
cb884abdc8 | ||
|
|
0a6b6fd18d | ||
|
|
8f359ef4c9 | ||
|
|
327d5a495e | ||
|
|
69925c6cde | ||
|
|
a5410642fa | ||
|
|
c3851f8e25 | ||
|
|
29f8797fbf | ||
|
|
9886d80b1c | ||
|
|
5bc709221e | ||
|
|
14f8a5f405 | ||
|
|
16d2a06d95 | ||
|
|
45f799c57d | ||
|
|
e814122c73 | ||
|
|
d45f2fc6cb | ||
|
|
c2319739e2 |
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mc2-memory": {
|
||||||
|
"command": "python",
|
||||||
|
"args": [
|
||||||
|
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_memory.py"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mc2-system": {
|
||||||
|
"command": "python",
|
||||||
|
"args": [
|
||||||
|
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_mc.py"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mc2-web": {
|
||||||
|
"command": "python",
|
||||||
|
"args": [
|
||||||
|
"F:\\Coding Stuff\\mission-control-2\\mcp\\mcp_web.py"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,29 @@ jobs:
|
|||||||
rot=1
|
rot=1
|
||||||
else
|
else
|
||||||
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
( cd frontend && npm ci --no-audit --no-fund && npm run build ) || rot=1
|
||||||
|
|
||||||
|
if [ $rot -eq 0 ]; then
|
||||||
|
echo "== Frontend: Push nach release-dist =="
|
||||||
|
git config --global user.name "Gitea Actions"
|
||||||
|
git config --global user.email "actions@gitea.local"
|
||||||
|
CURRENT_COMMIT=$(git rev-parse HEAD)
|
||||||
|
# Erzeuge (oder hole) den orphan branch 'release-dist'
|
||||||
|
git checkout --orphan release-dist 2>/dev/null || git checkout release-dist
|
||||||
|
git rm -rf . >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Checkout nur frontend/dist vom aktuellen Stand
|
||||||
|
git add -f frontend/dist
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD; then
|
||||||
|
git commit -m "Automatischer Frontend Build ($CURRENT_COMMIT) [skip ci]"
|
||||||
|
# Push to release-dist. Token auth from runner is expected to work for Gitea Actions.
|
||||||
|
git push origin HEAD:release-dist -f
|
||||||
|
else
|
||||||
|
echo "Keine Änderungen am Frontend-Build."
|
||||||
|
fi
|
||||||
|
# Zurück zum originalen Branch für den Rest des Skripts
|
||||||
|
git checkout $CURRENT_COMMIT
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -22,4 +22,3 @@ gemma_swap*
|
|||||||
|
|
||||||
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||||
frontend/tsconfig.tsbuildinfo
|
frontend/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ Kurz gehalten, nur die Wahrheiten, die man sonst schmerzhaft lernt. Details: `RE
|
|||||||
## Was das ist
|
## Was das ist
|
||||||
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
Lokaler Local-AI-Stack für die Box (Bosgame M5, Strix Halo, Vulkan/RADV). Schichten:
|
||||||
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
|
Engine (llama-swap) · Builtin-Routing-Gateway (`model: auto`) · MC2 (FastAPI + React/shadcn) ·
|
||||||
Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
**Hermes-Agent (das autonome Gehirn "Lucy")**. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadcn/Tailwind.
|
||||||
|
|
||||||
|
**Hardware-Spezifikationen der Box (WICHTIG für alle Agenten):**
|
||||||
|
- **System:** Bosgame M5 (AMD Strix Halo APU)
|
||||||
|
- **Arbeitsspeicher (RAM/VRAM):** 128 GB Shared Memory (ca. 122.7 GB nutzbar).
|
||||||
|
- **Inference-Limit:** Modelle im GGUF-Format dürfen maximal ca. 100-110 GB groß sein (entspricht ca. 150B Parametern bei Q4_K_M). Größere Modelle (wie 200B+ oder 2T Parameter) **können lokal nicht ausgeführt werden** und sind auszuschließen, es sei denn, es handelt sich um stark quantisierte MoEs.
|
||||||
|
**Gedächtnis & Dienste:** Hermes ist das autonome Agenten-Gehirn („Lucy"). Mem0 läuft als dedizierter semantischer Langzeit-Gedächtnis-Sidecar (`mem0-service` auf `:8765`) und wird von Hermes via MCP (`mcp_memory.py`) eingebunden. Governor, Zed-Workflows und alte Mittelschichten sind komplett gelöscht.
|
||||||
|
|
||||||
## Sprache (nicht verhandelbar)
|
## 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,
|
||||||
@@ -23,6 +29,12 @@ Hermes-Agent. Backend Python (Box läuft Python 3.14), Frontend Vite/React/shadc
|
|||||||
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
|
dem Deploy auf Gitea liegen**, und uncommittete Box-Änderungen gehen verloren (Absicht).
|
||||||
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
- **Nie direkt auf `main` arbeiten.** Immer Branch (`wartung/...`), Gate grün, dann Merge/Deploy.
|
||||||
|
|
||||||
|
## Agentic IDE & Vibe Coding
|
||||||
|
- **Zero Middle-Layers:** Coding passiert zu 100% lokal auf dem Dev-PC in der **OpenCode Desktop IDE**.
|
||||||
|
- Es gibt keinen Zed-Workflow, keine OpenCode-CLI-Mittelschicht und keinen Governor mehr.
|
||||||
|
- Der Agent in OpenCode Desktop nutzt via MCP (`.agents/mcp_config.json`) die API der Box (`:9001/v1`), um autonom Projekte zu bauen.
|
||||||
|
- **Mix-Ansatz beim Testen:** Der Agent testet lokal. Vor dem Push muss er prüfen, ob das Gitea-`VERIFY`-Skript fehlerfrei durchläuft.
|
||||||
|
|
||||||
## Zeit & Umgebung
|
## Zeit & Umgebung
|
||||||
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
- **Box = Ubuntu, läuft in `Europe/Berlin`** (seit 03.07.2026; vorher UTC). Dev-PC = Windows.
|
||||||
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
Naive/lokale Zeiten immer über `MC_LOCAL_TZ` (= `Europe/Berlin`) auflösen, nie `datetime.now()` ohne TZ annehmen
|
||||||
|
|||||||
@@ -12,54 +12,55 @@ prüft sich selbst und meldet sich, wenn etwas nicht stimmt.
|
|||||||
| Bahn | Was sie tut | Wo |
|
| Bahn | Was sie tut | Wo |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
|
| **Steuerzentrale** | Modelle, Routing, Wartung, Gedächtnis, Ideen-Queue, Auftragsbuch | MC2 `:9001` |
|
||||||
| **Coding** | Zed + OpenCode am Tag, `opencode-lauf.sh` in der Nacht — **ein Regelwerk, zwei Auslöser** | Governor `:8100` |
|
| **Coding** | Agentic IDE (z.B. OpenCode Desktop) auf Dev-PC via lokaler API + MCP | 100% lokal |
|
||||||
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
|
| **Lucy** | Sprach-Companion mit 3D-Avatar, Augen und Ohren | eigenes Repo `Hitonabi/lucy` |
|
||||||
|
|
||||||
Lucy holt ihr Hirn über MC2, spricht aber nie durch den Governor — ein Gespräch ist keine
|
Lucy holt ihr Hirn direkt über das MC2-Gateway (`:9010/v1`) mit nativer Bildweiche und Voice-Streaming.
|
||||||
Bau-Sitzung und wird nie unterbrochen.
|
|
||||||
|
|
||||||
## Ports & Dienste
|
## Ports & Dienste
|
||||||
|
|
||||||
| Port | Dienst | Erreichbar |
|
| Port | Dienst | Erreichbar |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `9001` | **MC2** (FastAPI + React) — Cockpit, API, Konsole | LAN |
|
| `9001` | **MC2** (FastAPI + React) — Cockpit, API, Konsole | LAN |
|
||||||
| `8100` | **Governor** — Token-Wächter vor dem Gateway | LAN |
|
|
||||||
| `8080` | **llama-swap** — Modell-Router | LAN |
|
| `8080` | **llama-swap** — Modell-Router | LAN |
|
||||||
| `9010` | `mc2-gateway` — `/v1`-Datenpfad (`model: auto`, Bild-Weiche) | loopback |
|
| `9010` | `mc2-gateway` — `/v1`-Datenpfad (`model: auto`, Bild-Weiche) | loopback |
|
||||||
| `8642` · `9119` | Hermes-Gateway · Hermes-Web-UI | loopback |
|
| `8642` · `9119` | Hermes-Gateway · Hermes-Web-UI | loopback |
|
||||||
| `8765` · `8650` | Mem0-Sidecar · Voice-Sidecar (STT/TTS) | loopback |
|
| `8765` · `8650` | Mem0-Sidecar · Voice-Sidecar (STT/TTS) | loopback |
|
||||||
| `7682` | ttyd — Box-Konsole, same-origin unter `/console/` | loopback |
|
| `7682` | ttyd — Box-Konsole, same-origin unter `/console/` | loopback |
|
||||||
|
|
||||||
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` · `governor` ·
|
Units in [`deploy/`](deploy/): `mission-control-2` · `mc2-gateway` · `mc2-steward` ·
|
||||||
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
|
`mem0-service` · `voice-service` · `box-console` · `hermes-builtin-ui` + Timer für Backup,
|
||||||
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
|
Auto-Update, Self-Smoke und Bagatell-Annahme. `llama-swap` läuft als System-Unit.
|
||||||
|
|
||||||
## Der Weg einer Idee
|
## Die Bau-Pipeline
|
||||||
|
|
||||||
```
|
```
|
||||||
Idee in MC2 → Gitea-Repo (mit CI-Ampel + VERIFY) → in Zed bauen → Ampel → main
|
Idee in MC2 → Gitea-Repo (mit CI-Ampel + VERIFY) → in Agentic IDE bauen → Ampel → main
|
||||||
└── oder nachts: Hermes-Cron → opencode-lauf.sh → dieselben Regeln
|
└── autonomes Vibe Coding via OpenCode Desktop + MCP
|
||||||
```
|
```
|
||||||
|
|
||||||
Jedes neue Repo wird von [`deploy/gitea-repo-create.sh`](deploy/gitea-repo-create.sh) **mit beiden
|
Jedes neue Repo wird von [`deploy/gitea-repo-create.sh`](deploy/gitea-repo-create.sh) **mit beiden
|
||||||
Wächtern geboren**:
|
Wächtern geboren**:
|
||||||
|
|
||||||
- **`.gitea/workflows/ci.yml`** — die Außen-Prüfung nach dem Push (Gitea Actions)
|
- **`.gitea/workflows/ci.yml`** — die Außen-Prüfung nach dem Push (Gitea Actions)
|
||||||
- **`VERIFY`** — die Innen-Prüfung: eine Zeile, wie man das Projekt testet. Das OpenCode-Plugin
|
- **`VERIFY`** — die Innen-Prüfung: eine Zeile, wie man das Projekt testet. Die Agentic IDE
|
||||||
führt sie nach jeder Etappe aus und gibt rote Tests dem Agenten **sofort** zurück, statt sie
|
führt sie im Mix-Ansatz vor jedem Push aus, um Code-Fehler ("Quality Gap") abzufangen.
|
||||||
erst der CI zu zeigen. Keine `VERIFY`-Datei = Prüf-Tor aus.
|
Keine `VERIFY`-Datei = Prüf-Tor aus.
|
||||||
|
|
||||||
## Modelle & Rollen
|
## Modelle & Rollen
|
||||||
|
|
||||||
Gemessen auf der Box (25.07.2026, Vulkan, Q4):
|
Gemessen auf der Box (Stand: August 2026, Vulkan b10502):
|
||||||
|
|
||||||
| 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 |
|
| `coder` | Qwen3-Coder-Next (80B-A3B) | **51,5 t/s** · Prefill **754 t/s** | Plant und baut — Kopf der Coding-Mannschaft (131k Kontext) |
|
||||||
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6 t/s** | Lucys Hirn **und** der Sucher-Subagent. Immer warm |
|
| `hermes` / `fast` | Qwen3.6-35B-A3B | **69,6–90 t/s** | Lucys Hirn **und** Sucher-Subagent. Immer warm |
|
||||||
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265–318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen) |
|
| `debugger` / `doctor` | Muse-Glimmer-30B | **40–50 t/s** (DFlash) | Runtime-Debugger & Fehler-Diagnostiker (Multimodal) |
|
||||||
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen |
|
| `kritiker` | Devstral-Small-2-24B | **15,0 t/s** · Prefill **265–318 t/s** | Liest gegen — bewusst **fremde Modellfamilie** (Mistral statt Qwen, 16k Deckel) |
|
||||||
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30). **Nie tagsüber** — verdrängt das warme Set |
|
| `vision` | Qwen3-VL-30B-A3B | auf Abruf | Lucys Augen (On-Demand) |
|
||||||
|
| `scout` | GLM-4.6V-Flash | auf Abruf | Schneller Vision- und Tool-Allrounder |
|
||||||
|
| `heavy` | gpt-oss-120b | nur nachts | Chef-Gutachter (4:30 Uhr). **Nie tagsüber** — verdrängt das warme Set |
|
||||||
|
| `dense-planer` | Qwen3.8-27B | **12,7 t/s** (On-Demand) | Dichtes 27B-Modell für tiefes Reasoning & 262k Kontext |
|
||||||
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
| `embed` · `reranker` | Qwen3 0.6B | immer warm | Gedächtnis + Feinsortierung |
|
||||||
|
|
||||||
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
‼️ **Der Kritiker ist bewusst auf 16k Kontext gedeckelt.** Devstral ist ein *dichtes* Modell —
|
||||||
@@ -75,13 +76,8 @@ Messwerte: [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md).
|
|||||||
|
|
||||||
## Qualitäts-Tore
|
## Qualitäts-Tore
|
||||||
|
|
||||||
1. **Werkzeug-Zaun** ([`deploy/opencode/plugin/`](deploy/opencode/plugin/)) — blockt `git push`,
|
1. **Prüf-Tor** — `VERIFY` nach jeder Etappe; rot → der Agent (OpenCode Desktop) repariert lokal nach.
|
||||||
`rm -rf`, `sudo`, `curl | sh` und Fremd-Hosts im Agentenlauf.
|
2. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft in Gitea.
|
||||||
2. **Prüf-Tor** — `VERIFY` nach jeder Etappe; rot → der Agent repariert selbst weiter (max. 3 Runden).
|
|
||||||
3. **Governor** ([`deploy/governor/`](deploy/governor/)) — zählt Tokens ehrlich (aus den echten
|
|
||||||
`usage.prompt_tokens` jeder Antwort, selbstkalibrierend). Bei ~45.000: Savepoint schreiben und
|
|
||||||
Sitzung sauber beenden. Bei ~50.000: harter Riegel. Sichtbar als Kachel im Cockpit.
|
|
||||||
4. **CI-Ampel** — Lint (ruff) · Import/Syntax (compileall) · Frontend-Build. Läuft bei jedem Push.
|
|
||||||
|
|
||||||
Gemeinsame Lehre dahinter: **Wissen lebt in Datei + git, nicht im schrumpfenden Chat-Kontext.**
|
Gemeinsame Lehre dahinter: **Wissen lebt in Datei + git, nicht im schrumpfenden Chat-Kontext.**
|
||||||
Kontext-Komprimierung löscht still die Regeln mit.
|
Kontext-Komprimierung löscht still die Regeln mit.
|
||||||
@@ -98,7 +94,7 @@ Kontext-Komprimierung löscht still die Regeln mit.
|
|||||||
## API (Auswahl)
|
## API (Auswahl)
|
||||||
|
|
||||||
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
|
`health · models/* · discover · fit · groups · routing/* · system/* · maintenance/* · connect ·
|
||||||
memory/* · agent/* · **governor** · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
|
memory/* · agent/* · ideen/* · auftragsbuch/* · chronik · eigenleben · wissen ·
|
||||||
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
|
zeitmaschine/* · reminders/* · voice/* · events (SSE)`
|
||||||
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
|
OpenAI-kompatibel: `/v1/chat/completions`, `/v1/completions`. MCP-Server: [`mcp/`](mcp/).
|
||||||
|
|
||||||
@@ -128,13 +124,12 @@ Dienst-Neustart. Vor jedem Commit lohnt der Ampel-Vorlauf: `ruff check .` und `n
|
|||||||
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine/Router |
|
| `MC_LLAMA_SWAP_URL` | `http://127.0.0.1:8080` | Engine/Router |
|
||||||
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap-Config |
|
| `MC_CONFIG_PATH` | `/etc/llama-swap/config.yaml` | llama-swap-Config |
|
||||||
| `MC_V1_UPSTREAM` | – | gesetzt → `/v1` geht an `mc2-gateway` (`:9010`) statt in-process |
|
| `MC_V1_UPSTREAM` | – | gesetzt → `/v1` geht an `mc2-gateway` (`:9010`) statt in-process |
|
||||||
| `MC_GOVERNOR_URL` | `http://127.0.0.1:8100` | Token-Wächter für die Cockpit-Kachel |
|
|
||||||
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | aktive Engine (Vulkan-Build) |
|
| `MC_ENGINE_PATH` | `/opt/llamacpp-vulkan` | aktive Engine (Vulkan-Build) |
|
||||||
| `MC_REWARM_ENABLED` | `1` | Warm-Set-Wächter (auf der Box bewusst `0`) |
|
| `MC_REWARM_ENABLED` | `1` | Warm-Set-Wächter (auf der Box bewusst `0`) |
|
||||||
| `MC_DRAFTS_DIR` · `MC_SPEC_TYPE` | `$MODELS/drafts` · `draft-simple` | Spekulatives Dekodieren |
|
| `MC_DRAFTS_DIR` · `MC_SPEC_TYPE` | `$MODELS/drafts` · `draft-simple` | Spekulatives Dekodieren |
|
||||||
|
|
||||||
Governor-eigene Schalter (`GOV_THRESHOLD`, `GOV_HARD_CEILING`, `GOV_EXEMPT_MODELS`, …):
|
|
||||||
[`deploy/governor/README.md`](deploy/governor/README.md).
|
|
||||||
|
|
||||||
## Weiterlesen
|
## Weiterlesen
|
||||||
|
|
||||||
@@ -146,5 +141,5 @@ Governor-eigene Schalter (`GOV_THRESHOLD`, `GOV_HARD_CEILING`, `GOV_EXEMPT_MODEL
|
|||||||
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
|
| [`docs/AUFTRAGSBUCH.md`](docs/AUFTRAGSBUCH.md) | Vorschlags-Inbox und das Ein-Klick-Gate |
|
||||||
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
|
| [`docs/wissen/VERDIKTE.md`](docs/wissen/VERDIKTE.md) | **Finale Technik-Entscheide — nicht neu aufrollen** |
|
||||||
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
|
| [`docs/wissen/FALLEN.md`](docs/wissen/FALLEN.md) · [`docs/wissen/OFFENE-FAEDEN.md`](docs/wissen/OFFENE-FAEDEN.md) | Stolpersteine · offene Punkte |
|
||||||
| [`deploy/opencode/README.md`](deploy/opencode/README.md) | Coding-Bahn: Mannschaft, Verteilung, Fallen |
|
|
||||||
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
|
| [`AGENTS.md`](AGENTS.md) | Arbeitsregeln für Agenten in diesem Repo |
|
||||||
|
|||||||
+176
-50
@@ -1,50 +1,176 @@
|
|||||||
# Savepoint — Mission Control 2.0 (MC2)
|
# Savepoint — Mem0-Ausbau (Referenzaufgabe)
|
||||||
|
|
||||||
_Stand: 2026-07-05. Zustands-Snapshot für einen externen Code-Review (Antigravity). Zuerst
|
**Stand:** 2026-08-22, Worktree `referenz/mem0-ausbau` in `F:\Coding Stuff\mc2-referenz`.
|
||||||
`AGENTS.md` lesen (Projekt-Regeln), dann diese Datei (wo wir stehen), dann `README.md`/`docs/`._
|
Branch wurde aus `d508a84` (A2's gesicherter Stand) erstellt und enthält dessen 5 bearbeitete Dateien.
|
||||||
|
|
||||||
## Was das ist (in einem Satz)
|
## Auftrag
|
||||||
MC2 ist die **Web-Admin-Konsole** einer 100 % lokalen KI-Appliance („die Box", Bosgame M5 / AMD
|
`docs/aufgaben/referenzaufgabe-mem0-ausbau.md`: Mem0-Reste vollständig entfernen, ohne MC2/Frontend/Deployment zu brechen.
|
||||||
Strix Halo, 128 GB Unified RAM, llama.cpp-Vulkan). Sie verwaltet Engine, Modelle, Gedächtnis,
|
- Messlatte: `ruff` clean, Backend `import app` sauber, `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` = 0 Dateien, `mem0_service/` entfernt, Box-Smoke/Health so weit wie möglich grün.
|
||||||
Agenten-Status, Updates & Wartung — und dient teils als Gedächtnis-Oberfläche für die Sprach-
|
- **Prüfstein:** API-Felder wie `mem0_ms` nicht einfach löschen — Frontend mit anpassen oder Feld sauber aus beiden Seiten entfernen.
|
||||||
Begleiterin „Lucy".
|
- Arbeitsweise: Datei für Datei, höchstens zwei vor der ersten Änderung. Nach jeder Änderung ein Satz.
|
||||||
|
- Live-Deployment `~/mission-control-v2` ist TABU.
|
||||||
## Architektur (Kurzform — Details in AGENTS.md/docs/)
|
|
||||||
- **Backend** `backend/` — FastAPI, `:9001` als systemd-**User-Dienst** (reboot-fest, sudo-frei).
|
## Wichtig: A2-Vorgang (Commit d508a84 — bereits auf diesem Branch)
|
||||||
Router (`routers/`, dünn) → Logik (`services/`, Single Source of Truth).
|
Diese 5 Dateien wurden vom abgebrochenen Lauf A2 bereits bearbeitet und im Commit `d508a84` gesichert:
|
||||||
- **Frontend** `frontend/` — React/Vite/Tailwind/shadcn. **`frontend/dist` ist ABSICHT im git**
|
1. `backend/config.py` — `MEMORY_DB` und `MC_MEM_DEDUPE_ENABLED` entfernt, `MEM0_SERVICE_URL`-Kommentar bereinigt
|
||||||
(Box hat kein Node; Backend liefert die gebauten Assets aus).
|
2. `backend/routers/system.py` — `_mem0_reachable()` entfernt, `mem0_reachable` aus Systeminfo
|
||||||
- **Eingebautes Gateway** `:9001/v1` (OpenAI-kompatibel) → llama-swap `:8080` (Engine, Vulkan/RADV).
|
3. `backend/routers/voice.py` — `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||||
- **Sidecars:** `mem0_service/` (Mem0-Gedächtnis), `voice_service/` (STT/TTS für den „Sprechen"-Tab),
|
4. `backend/routers/zeitmaschine.py` — `Mem0` in Docstrings/Kommentaren → `Gedächtnis`
|
||||||
`mcp/` (MCP-Server), `hermes/` (Hermes-Agent-Plugin), `client/hermes-pc` (PC-Executor).
|
5. `backend/services/sentry.py` — `MC_MEM0_DASHBOARD_URL` in Docstring entfernt
|
||||||
|
|
||||||
## Aktueller Zustand
|
`git status` war nach d508a84 clean.
|
||||||
- **Stabil, in Produktion, „MC2 Final".** Ein Neubau („MC3") wurde bewusst VERWORFEN — MC2 wird
|
|
||||||
sauber gehalten und eingefroren, nicht neu erfunden.
|
## Wichtig: Kahlschlag-Commit 1e68f62 (bereits auf main)
|
||||||
- **Autonomie-Ausbau abgeschlossen** (Observability-Latenztrace, Fremd-Modell-Kritiker/Härtung,
|
- `backend/services/memory.py` — gelöscht
|
||||||
nächtliches „Dreaming" mit Wissens-Vault) — jeweils propose-only, Mensch = Gate.
|
- `backend/routers/memory.py` — gelöscht (`/api/memory` existiert nicht mehr)
|
||||||
- **Zuletzt (Commit `ff1b9a0`, 05.07.):** `coder-lite` (Qwen3-Coder-30B) entfernt; der Verbinden-Tab
|
- `deploy/mc2-steward.service` — gelöscht
|
||||||
(`services/connect.py`) empfiehlt IDEs jetzt die realen Direkt-Modelle `coder`/`heavy`/`vision`
|
- `deploy/mem0-service.service` — gelöscht
|
||||||
statt der virtuellen „coding"-Lane. Davor u. a. Engine-Update-Fix, Hermes-Terminal same-origin.
|
- `deploy/voice-service.service` — gelöscht
|
||||||
- **Modelle live:** hermes=Qwen3.6-35B-A3B (Agent-Hirn, immer warm) · coder=Qwen3-Coder-Next ·
|
- `deploy/mem0-konsolidierung.*` — gelöscht
|
||||||
heavy=gpt-oss-120b · vision=Qwen3-VL-30B · scout=GLM-4.6V · embed. Warm-Set = hermes+vision+embed.
|
|
||||||
|
## Erledigt in dieser Sitzung
|
||||||
## Nordstern & Randbedingungen (WICHTIG für den Review)
|
|
||||||
- **Zieht ohne Wartung über JAHRE durch.** Robustheit + Einfachheit schlagen Features. Der Betreiber
|
### 1. `backend/steward.py`
|
||||||
ist **kein Entwickler** — alles muss reboot-/update-fest und selbsterklärend sein.
|
- Toter Import `from services import memory as memory_svc` entfernt
|
||||||
- **100 % lokal, kein Cloud-Zwang** (Privatsphäre ist der Sinn). Hardware-Decke: ~124 GB, ein Topf.
|
- Toter `memory_svc.AUTO_DEDUPE_ENABLED`-Block entfernt
|
||||||
- **Lucy ist ein SEPARATES Repo** (`F:\Coding Stuff\lucy`) — **nicht Teil dieses Reviews.**
|
- Docstring: `Mem0-Auto-Dedupe` → `Re-Warm-Wächter und Health-Wächter (sentry)`
|
||||||
|
- Docstring: `memory.auto_dedupe_loop — Gedächtnis-Dubletten` entfernt
|
||||||
## Bekannte raue Kanten (Kandidaten — gern bestätigen/erweitern)
|
- Docstring: `MC_MEM_DEDUPE_ENABLED` entfernt
|
||||||
- Doku-Drift möglich: `AGENTS.md` nennt „Box läuft in UTC" — die Box wurde inzwischen auf
|
- Verifiziert: `memory.py` wurde in Kahlschlag gelöscht, `steward.py` hatte toten Import
|
||||||
`Europe/Berlin` umgestellt.
|
|
||||||
- Alte, disabled v1-System-Unit `mission-control.service` liegt kosmetisch herum (sudo zum Entfernen).
|
### 2. `backend/services/voice_metrics.py` (alle 9 Treffer bereinigt)
|
||||||
- Ein paar frühere Frontend-Monolithen wurden entflochten; Rest-Altlasten möglich.
|
- **Docstring (3x):** `Mem0-Retrieve` → `Gedächtnis-Retrieve`
|
||||||
|
- **Line 10:** `Mem0-Retrieve` im Best-effort-Kommentar → `Gedächtnis-Retrieve`
|
||||||
## Was NICHT empfohlen werden soll (bereits entschieden)
|
- **Line 39:** `Mem0-Retrieve` im docstring → `Gedächtnis-Retrieve`
|
||||||
Cloud-Migration · Voll-Rewrite · schwere neue Frameworks · Engine-/Modell-Wechsel, die nicht in
|
- **Line 56:** `Geparkten Mem0-Retrieve` → `Geparkten Gedächtnis-Retrieve`
|
||||||
124 GB passen · `frontend/dist` aus git nehmen (Absicht) · Security-Config anfassen. Erwünscht sind
|
- **Line 106:** `Unter-Detail: mem0 (Teil VON hirn...` → `Unter-Detail: memory (Teil VON hirn...`
|
||||||
**Bugs, Fragilität, tote Pfade, Sicherheitslücken, Vereinfachung (KISS/DRY), Wartbarkeit.**
|
- **Line 116:** `Agent + Mem0 + TTFT` → `Agent + Gedächtnis + TTFT`
|
||||||
|
- **Line 133:** `Agent-Overhead + Mem0 + LLM-TTFT` → `Agent-Overhead + Gedächtnis + LLM-TTFT`
|
||||||
## Letzter Sicherungspunkt
|
- **Line 141:** `mem0 = _take_retrieve(...)` → `memory = _take_retrieve(...)` (starker Kommentar)
|
||||||
- git `ff1b9a0` „coder-lite raus + Verbinden-Tab auf echte Direkt-Modelle" (auf `main`, deployt, live).
|
- **Line 154:** `"mem0_ms"` → `"memory_ms"` (API-Feld im JSON-Rückgabewort)
|
||||||
|
|
||||||
|
### 3. `frontend/src/lib/api.ts` (3 Treffer)
|
||||||
|
- **Line 446:** `mem0 = Unter-Detail INNERHALB von hirn` → `memory = Unter-Detail INNERHALB von hirn`
|
||||||
|
- **Line 455:** `Agent + Mem0 + LLM-TTFT` → `Agent + Gedächtnis + LLM-TTFT`
|
||||||
|
- **Line 457:** `mem0_ms: number | null // ... (Mem0-Retrieve)` → `memory_ms: number | null // ... (Gedächtnis-Retrieve)`
|
||||||
|
|
||||||
|
### 4. `frontend/src/components/dashboard/LatencyCard.tsx` (4 Treffer)
|
||||||
|
- **Line 5:** `Mem0 ist ein Unter-Detail` → `Gedächtnis ist ein Unter-Detail`
|
||||||
|
- **Line 42:** `t.mem0_ms` (2x) → `t.memory_ms` (2x)
|
||||||
|
- **Line 106:** `latest.mem0_ms` (2x) → `latest.memory_ms` (2x)
|
||||||
|
- **Line 131:** `Agent + Mem0-Suche` → `Agent + Gedächtnis-Suche`
|
||||||
|
|
||||||
|
### 5. `frontend/src/views/GuideView.tsx` (1 Treffer, kompletter Paragraph)
|
||||||
|
- Stack-Paragraph (Card 8 "Gedächtnis & RAG"): "Dein Gedächtnis ist Mem0-basiert: auto-lernend..." umgeschrieben zu "Dein Gedächtnis ist auto-lernend & semantisch: Der Agent lernt im Hintergrund nach jedem Turn..."
|
||||||
|
- Gotcha-Zeile (Mem0-extraktion) entfernt
|
||||||
|
- Begründung: Gedächtnis-Tab existiert nicht mehr (Kahlschlag), Mem0 wurde durch Hermes-Nativ abgelöst
|
||||||
|
|
||||||
|
### 6. `backend/services/backup.py` (1 Treffer)
|
||||||
|
- Docstring: `Full state backup (mem0 + ...)` → `Full state backup (...)`
|
||||||
|
|
||||||
|
### 7. `deploy/backup.sh` (8 Treffer)
|
||||||
|
- **Line 6-7:** Kommentar `mem0 (Chroma + history.db)` aus Inhalt entfernt
|
||||||
|
- **Line 23:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — ganze Zeile gelöscht
|
||||||
|
- **Line 30:** `mkdir -p "$STAGE/mem0"` — aus mkdir entfernt
|
||||||
|
- **Line 32:** `[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true` — ganze Zeile gelöscht
|
||||||
|
- **Line 63:** Kommentar `mem0-venv ist uv-managed` → `voice-venv ist uv-managed`
|
||||||
|
- **Line 71:** `freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"` — ganze Zeile gelöscht
|
||||||
|
- **Line 82:** `$HOME/.mem0/venv/bin/python` aus for-Schleife entfernt
|
||||||
|
- **Line 95:** MANIFEST: `mem0 (chroma + history.db)` aus Inhalt entfernt
|
||||||
|
|
||||||
|
### 8. `mcp/mcp_mc.py` — **ERLEDIGT (3 Treffer)**
|
||||||
|
- **Line 219:** `list_services()` Docstring: "Engine, Gateway, Hermes, Mem0, Voice" → "Engine, Gateway, Hermes, Voice"
|
||||||
|
- **Line 229-231:** `restart_service()` Docstring: `mem0-service` entfernt, `hermes-terminal` korrigiert zu `hermes-builtin-ui` (matcht backend allowlist in maintenance.py)
|
||||||
|
- **Line 238:** `backup_now()` Docstring: "mem0 + Configs + Secrets" → "Configs + Secrets"
|
||||||
|
- Verifiziert: `rg -i mem0 mcp/mcp_mc.py` = 0 Treffer, `py_compile mcp/mcp_mc.py` OK
|
||||||
|
- **Achtung:** Der mc2-memory-Plugin (`hermes/plugins/mc2-memory/__init__.py`) ruft noch `/api/memory` und `/api/memory/learn` auf. Der Kahlschlag-Commit hat `backend/routers/memory.py` gelöscht → dieser API-Pfad existiert nicht mehr. Der Plugin degraded silently (prefetch="", learn fail). **Entscheidung:** Nicht bearbeiten, Plugin ist im `hermes/plugins/`-Ordner, nicht in der Messlatte.
|
||||||
|
|
||||||
|
### 9. `deploy/agent-hooks/box-steckbrief-inject.sh`
|
||||||
|
- Treffer: "mem0-Gedaechtnis :8765" in einer langen Agent-Injektionszeile (Zeile 31)
|
||||||
|
- Nur Text → mem0-Gedaechtnis :8765 entfernen
|
||||||
|
|
||||||
|
### 10. `deploy/autoupdate.sh`
|
||||||
|
- Treffer: "Gehirn-Check (Mem0, Tools, Voice)" → "Gehirn-Check (Tools, Voice)"
|
||||||
|
|
||||||
|
### 11. `deploy/hermes-postcheck.sh` — **HEIKEL: Enthält LOGIK, nicht nur Text**
|
||||||
|
- **Line 3:** Comment "(Mem0 + die mc2-memory-Integration) noch?" → muss aktualisiert werden
|
||||||
|
- **Line 8:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable für Sidecar-Check (Zeilen 14-18)
|
||||||
|
- **Zeilen 14-18:** curl-Check auf `:8765/health` → **wird immer FAILn**, da mem0_service stillgelegt
|
||||||
|
- **Zeilen 20-24:** curl-Check auf `/api/memory` → **wird immer FAILn**, da memory.py in Kahlschlag gelöscht
|
||||||
|
- **Zeilen 26-31:** grep-Check für `memory.provider=mc2-memory` in config.yaml → das ist **neues** Hermes-nativ-Memory, kein Mem0-Rest. **NICHT entfernen** (gehört zur aktuellen Architektur).
|
||||||
|
- **Zeilen 52-58:** Plugin-Check für `mc2-memory` → ebenfalls **neue** Architektur, kein Mem0-Rest. **NICHT entfernen**.
|
||||||
|
- **Entscheidung:** Zeilen 8, 14-18, 20-24 entfernen (toter Sidecar + toter /api/memory-Endpunkt). Zeilen 26-31 und 52-58 behalten (gehören zur neuen Hermes-native Memory).
|
||||||
|
|
||||||
|
### 12. `deploy/restore.sh` — **LOGIK: Gegenstück zu backup.sh**
|
||||||
|
- **Line 18:** `MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"` — Variable
|
||||||
|
- **Line 22:** `SERVICES="mem0-service mission-control-2 hermes-gateway"` → "mem0-service" entfernen
|
||||||
|
- **Line 101:** dry-run echo "mem0 → $MEM0_DIR" — mem0-Zeile entfernen
|
||||||
|
- **Line 122:** `if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir ...; cp ...; fi` — ganze Zeile entfernen
|
||||||
|
- Da backup.sh kein `$STAGE/mem0` mehr erstellt, ist diese Logik tot (würde nie feuern). Entfernen.
|
||||||
|
|
||||||
|
### 13. `deploy/stack-postcheck.sh` — **LOGIK: Enthält toten curl-Check**
|
||||||
|
- **Line 6:** Comment "prüft das Gehirn/Mem0" → "prüft das Gehirn"
|
||||||
|
- **Line 12:** `MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"` — Variable
|
||||||
|
- **Line 14:** Comment "Embedding-Modell (Mem0/Gedächtnis)" → "Embedding-Modell (Gedächtnis)"
|
||||||
|
- **Line 51:** Comment "Embedding-Modell (für Mem0/Gedächtnis)" → "Embedding-Modell (für Gedächtnis)"
|
||||||
|
- **Line 58:** "(Mem0/Gedächtnis betroffen)" → "(Gedächtnis betroffen)"
|
||||||
|
- **Zeilen 88-93:** curl-Check auf `MEM0_URL/health` → **wird immer FAILn**, da Sidecar tot. **Ganze Block entfernen.**
|
||||||
|
- Die Embedding-Checks (Zeilen 51-59) bleiben — sie prüfen das Embedding-Modell, das auch für die neue Memory-Architektur relevant ist.
|
||||||
|
|
||||||
|
### 14. `deploy/warmup.sh`
|
||||||
|
- Treffer: "für Mem0/Gedächtnis" → "für Gedächtnis" (nur Text)
|
||||||
|
|
||||||
|
### 15. `deploy/venv-audit.sh`
|
||||||
|
- **Line 2:** Comment "(Mem0, Voice) altern" → "(Voice) altern"
|
||||||
|
- **Line 12:** `VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")` → mem0-Entry entfernen
|
||||||
|
|
||||||
|
### 16. Ordner `mem0_service/` löschen
|
||||||
|
- Enthält: `app.py`, `migrate.py`, `smoke_test.py`, `requirements.txt`
|
||||||
|
- Toter Sidecar: Port `:8765` antwortet nicht, nicht systemd-registered
|
||||||
|
|
||||||
|
## Noch nicht touchen (out-of-scope / unklar)
|
||||||
|
|
||||||
|
### `voice_service/app.py` + `voice_service/install.sh`
|
||||||
|
- Enthalten "mem0" — gehören zur voice_service, nicht zur Messlatte
|
||||||
|
- voice_service.service wurde in Kahlschlag gelöscht, aber voice_service/ Ordner ist noch vorhanden
|
||||||
|
- **Entscheidung:** Nicht bearbeiten, da nicht in `backend/`, `mcp/`, `deploy/` Messlatte
|
||||||
|
|
||||||
|
### `hermes/plugins/mc2-memory/` (2 Dateien)
|
||||||
|
- `__init__.py`, `plugin.yaml`
|
||||||
|
- Rufen `/api/memory` auf → existiert nicht mehr, degradieren gracefully
|
||||||
|
- Nicht in Messlatte (`deploy/` mit `.py/.sh` — YAML zählt nicht, Python-Plugin ist extern)
|
||||||
|
- **Entscheidung:** Nicht bearbeiten (Hermes-Plugin, nicht Teil von MC2)
|
||||||
|
|
||||||
|
### `deploy/mission-control-2.service` + `deploy/mc2-backup.service`
|
||||||
|
- systemd Service-Dateien (keine `.py` oder `.sh`)
|
||||||
|
- Nicht in Messlatte, aber könnten Mem0-Referenzen haben
|
||||||
|
- **Entscheidung:** Prüfen, ob sie Mem0 nennen; wenn ja, Text anpassen
|
||||||
|
|
||||||
|
### `deploy/llama-swap.config.yaml`
|
||||||
|
- YAML, nicht in Messlatte
|
||||||
|
|
||||||
|
### `frontend/dist/`
|
||||||
|
- Muss nach Änderungen neu gebaut werden (`npm run build`)
|
||||||
|
- **NACHDEM alle .ts/.tsx Dateien bearbeitet sind**
|
||||||
|
|
||||||
|
## Bekannte Fallstricke
|
||||||
|
|
||||||
|
1. **Prüfstein (Frontend-Konsistenz):** `mem0_ms` war ein API-Feld, das das Frontend las. Wurde zu `memory_ms` umbenannt + Frontend angepasst. **Nicht einfach löschen.**
|
||||||
|
2. **Restore vs. Backup:** `restore.sh` ist das Gegenstück zu `backup.sh`. Wenn backup.sh mem0 weglässt, darf restore.sh nicht versuchen, mem0 zu restaurieren. **Sorgfältig prüfen!**
|
||||||
|
3. **`head` funktioniert in PowerShell nicht:** Muss `Select-Object -First N` verwenden.
|
||||||
|
4. **Globs für native Befehle:** `rg` mit Globs muss gequoted werden, sonst spaltet PowerShell falsch.
|
||||||
|
5. **`frontend/dist` ist im git:** Nach Frontend-Änderungen muss `npm run build` laufen und das neue `dist` committet werden.
|
||||||
|
6. **`/api/memory` existiert nicht mehr** (Kahlschlag), aber `frontend/src/lib/queries.ts` ruft es noch via `useMemory()`: `api<Memory[]>(`/api/memory`...`. Das ist ein **vorher bestehendes Problem** (nicht eingeführt in dieser Runde).
|
||||||
|
|
||||||
|
## Nächster Schritt
|
||||||
|
1. **mcp/mcp_mc.py committen** (bereits bearbeitet)
|
||||||
|
2. `deploy/agent-hooks/box-steckbrief-inject.sh` bearbeiten (1 Treffer, Text)
|
||||||
|
3. `deploy/autoupdate.sh`, `deploy/warmup.sh` bearbeiten (je 1 Treffer, Text)
|
||||||
|
4. `deploy/venv-audit.sh` bearbeiten (2 Treffer, Text + Venv-Liste)
|
||||||
|
5. `deploy/stack-postcheck.sh` bearbeiten (6 Treffer, 1 Block-Entfernung, 4 Kommentare)
|
||||||
|
6. `deploy/restore.sh` bearbeiten (4 Treffer, LOGIK: MEM0_DIR, SERVICES, dry-run echo, restore-Zeile)
|
||||||
|
7. `deploy/hermes-postcheck.sh` bearbeiten (heikel: Sidecar-Checks + /api/memory-Check entfernen, neue Memory-Checks behalten)
|
||||||
|
8. `mem0_service/` Ordner löschen
|
||||||
|
9. `ruff` + `import app` prüfen
|
||||||
|
10. `grep -ril mem0 backend/ mcp/ deploy/ --include='*.py' --include='*.sh'` → muss 0 ergeben
|
||||||
|
11. `frontend/dist` neu bauen (Box: `cd frontend && npm run build`)
|
||||||
|
|||||||
+4
-4
@@ -33,13 +33,14 @@ from routers import (
|
|||||||
maintenance,
|
maintenance,
|
||||||
models,
|
models,
|
||||||
routing,
|
routing,
|
||||||
|
skills,
|
||||||
system,
|
system,
|
||||||
voice,
|
voice,
|
||||||
wissen,
|
wissen,
|
||||||
zeitmaschine,
|
zeitmaschine,
|
||||||
)
|
)
|
||||||
from routers import reminders as reminders_router
|
from routers import reminders as reminders_router
|
||||||
from services import ketten_digest, metrics_history, reminders, sentry, warmer
|
from services import metrics_history, reminders, sentry, warmer
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
# Zentrales Logging — Level via MC_LOG_LEVEL (INFO default). Eine Konfiguration
|
||||||
@@ -65,9 +66,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
tasks.append(asyncio.create_task(reminders.reminders_loop()))
|
||||||
if ketten_digest.ENABLED:
|
|
||||||
# Werkstatt-Status aufs Handy: Fragen sofort, Meilensteine sofort, sonst 60-min-Puls.
|
|
||||||
tasks.append(asyncio.create_task(ketten_digest.digest_loop()))
|
|
||||||
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
|
# 24-h-Metrik-Verlauf (Cockpit-Zeitachse): 10-s-Sampler, Ringpuffer, persistiert.
|
||||||
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
|
tasks.append(asyncio.create_task(metrics_history.sampler_loop()))
|
||||||
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
# Geteilter HTTP-Client zur lokalen Engine: Keep-Alive/Connection-Pooling statt neuer Client
|
||||||
@@ -109,6 +107,7 @@ app.include_router(health.router)
|
|||||||
app.include_router(models.router)
|
app.include_router(models.router)
|
||||||
app.include_router(routing.router)
|
app.include_router(routing.router)
|
||||||
app.include_router(system.router)
|
app.include_router(system.router)
|
||||||
|
|
||||||
app.include_router(connect.router)
|
app.include_router(connect.router)
|
||||||
app.include_router(agent.router)
|
app.include_router(agent.router)
|
||||||
app.include_router(
|
app.include_router(
|
||||||
@@ -133,6 +132,7 @@ app.include_router(chronik.router) # Timeline der autonomen Taten (Announ
|
|||||||
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
app.include_router(events.router) # SSE-Eventstrom /api/events (P3a) — Invalidation-Bus
|
||||||
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
app.include_router(wissen.router) # Wissens-Vault (Traum-Notizen) read-only
|
||||||
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
app.include_router(zeitmaschine.router) # Snapshots ansehen + Ein-Klick-Restore (detached)
|
||||||
|
app.include_router(skills.router) # Skills & Jobs Dashboard
|
||||||
app.include_router(
|
app.include_router(
|
||||||
console.router
|
console.router
|
||||||
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
) # Box-Konsole (ttyd) same-origin durchreichen — VOR dem SPA-Catch-all
|
||||||
|
|||||||
+2
-9
@@ -19,13 +19,6 @@ MODELS_DIR = Path(os.environ.get("MC_MODELS_DIR", "/srv/models"))
|
|||||||
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
# Persistent neben den Modellen (übersteht Deploys). TTL = Frische-Fenster.
|
||||||
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
DISCOVER_CACHE_PATH = Path(os.environ.get("MC_DISCOVER_CACHE", str(MODELS_DIR / "mc2-discover.json")))
|
||||||
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
DISCOVER_TTL = int(os.environ.get("MC_DISCOVER_TTL", "43200")) # 12 h
|
||||||
# Geteiltes Gedächtnis (SQLite, WAL). Persistent neben den Modellen.
|
|
||||||
# Hinweis: nur noch für die einmalige Mem0-Migration relevant — das aktive Gedächtnis
|
|
||||||
# liegt jetzt in Mem0/Chroma hinter dem Sidecar (siehe MEM0_SERVICE_URL).
|
|
||||||
MEMORY_DB = Path(os.environ.get("MC_MEMORY_DB", str(MODELS_DIR / "mc2-memory.db")))
|
|
||||||
# Mem0-Sidecar (auto-lernendes, semantisches Gedächtnis). Läuft im ~/.mem0/venv (Python 3.12),
|
|
||||||
# weil mem0+chromadb unter dem 3.14-Backend nicht laufen. MC2 spricht ihn lokal per HTTP an.
|
|
||||||
MEM0_SERVICE_URL = os.environ.get("MC_MEM0_SERVICE_URL", "http://127.0.0.1:8765").rstrip("/")
|
|
||||||
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
# Befehl-Vorlage für llama-swap: {model}=GGUF-Pfad, {ctx}=Kontext, ${PORT} bleibt stehen.
|
||||||
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
# Hinweis: --prompt-cache/--prompt-cache-all sind llama-CLI-Flags, NICHT llama-server —
|
||||||
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
# llama-server lehnt sie ab ("invalid argument") und startet dann nicht. Prompt-Caching
|
||||||
@@ -72,7 +65,7 @@ V1_UPSTREAM = os.environ.get("MC_V1_UPSTREAM", "").rstrip("/")
|
|||||||
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
HERMES_API_URL = os.environ.get("HERMES_API_URL", "http://127.0.0.1:8642").rstrip("/")
|
||||||
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
# API-Key der Hermes-`api_server`-Plattform (~/.hermes/.env: API_SERVER_KEY). Nötig für
|
||||||
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
# /v1/chat/completions (Voice-Pipeline) — Bearer-Auth, sonst 401. Derselbe volle Agent
|
||||||
# (Tools + geteiltes Mem0) wie CLI/Telegram, nur über HTTP.
|
# (Tools + geteiltes Gedächtnis) wie CLI/Telegram, nur über HTTP.
|
||||||
def _read_hermes_env(key: str) -> str:
|
def _read_hermes_env(key: str) -> str:
|
||||||
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
"""Liest einen Schlüssel aus ~/.hermes/.env (Fallback, falls nicht in der Prozess-Env).
|
||||||
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
Der MC2-Dienst erbt die Hermes-Secrets sonst nicht."""
|
||||||
@@ -97,7 +90,7 @@ HERMES_API_KEY = (
|
|||||||
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
HERMES_API_MODEL = os.environ.get("HERMES_API_MODEL", "hermes")
|
||||||
|
|
||||||
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
# --- Voice-Sidecar (STT faster-whisper + TTS Piper/Chatterbox) ---------------
|
||||||
# Eigenes Python-3.12-venv (~/.voice/venv), analog Mem0-Sidecar. MC2 proxyt nach außen.
|
# Eigenes Python-3.12-venv (~/.voice/venv). MC2 proxyt nach außen.
|
||||||
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
VOICE_SERVICE_URL = os.environ.get("MC_VOICE_SERVICE_URL", "http://127.0.0.1:8650").rstrip("/")
|
||||||
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
# Box-Konsole: ttyd-Web-Terminal (echte Login-Shell). Bindet NUR an Loopback
|
||||||
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
# (127.0.0.1:7682, base-path /console) und wird von MC2 über den ohnehin offenen Port 9001
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
"_comment": "Kuratierter Modell-Katalog (Cookbook) für Strix Halo / Ryzen AI MAX+ 395 — 128GB unified, bandbreiten-limitiert (256 GB/s). MoE-first. EINE Quelle der Wahrheit für KORREKTE Metadaten (total/active params, moe, generation) → präzise Empfehlungen ohne Namens-Raterei. Inspiriert vom Odysseus-Cookbook (statischer, validierter Katalog statt Live-Scraping). Erweiterbar: neue Modelle hier eintragen. Felder: name (Match-Identifier), repo (HF org/name für Install), family (+Subtyp), generation (numerisch, für Upgrade-Vergleich), total_params_b, active_params_b (=total bei dense), moe, quant, ctx (empfohlen), tools, vision.",
|
"_comment": "Kuratierter Modell-Katalog (Cookbook) für Strix Halo / Ryzen AI MAX+ 395 — 128GB unified, bandbreiten-limitiert (256 GB/s). MoE-first. EINE Quelle der Wahrheit für KORREKTE Metadaten (total/active params, moe, generation) → präzise Empfehlungen ohne Namens-Raterei. Inspiriert vom Odysseus-Cookbook (statischer, validierter Katalog statt Live-Scraping). Erweiterbar: neue Modelle hier eintragen. Felder: name (Match-Identifier), repo (HF org/name für Install), family (+Subtyp), generation (numerisch, für Upgrade-Vergleich), total_params_b, active_params_b (=total bei dense), moe, quant, ctx (empfohlen), tools, vision.",
|
||||||
"version": "2026-07-03",
|
"version": "2026-07-03",
|
||||||
"models": [
|
"models": [
|
||||||
|
{
|
||||||
|
"role": "debugger", "name": "Muse-Glimmer-30B", "repo": "unsloth/Muse-Glimmer-30B-GGUF",
|
||||||
|
"family": "muse", "generation": 1.0, "total_params_b": 30, "active_params_b": 30,
|
||||||
|
"moe": false, "quant": "Q4_K_XL", "ctx": 65536, "tools": true, "vision": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "coder", "name": "Qwen3.8-27B", "repo": "unsloth/Qwen3.8-27B-GGUF",
|
||||||
|
"family": "qwen", "generation": 3.8, "total_params_b": 27, "active_params_b": 27,
|
||||||
|
"moe": false, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"role": "fast", "name": "Qwen3.6-35B-A3B", "repo": "Qwen/Qwen3.6-35B-A3B-GGUF",
|
"role": "fast", "name": "Qwen3.6-35B-A3B", "repo": "Qwen/Qwen3.6-35B-A3B-GGUF",
|
||||||
"family": "qwen", "generation": 3.6, "total_params_b": 35, "active_params_b": 3,
|
"family": "qwen", "generation": 3.6, "total_params_b": 35, "active_params_b": 3,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ router = APIRouter(prefix="/api")
|
|||||||
class IdeeIn(BaseModel):
|
class IdeeIn(BaseModel):
|
||||||
titel: str
|
titel: str
|
||||||
notiz: str = ""
|
notiz: str = ""
|
||||||
welt: str = "box" # "box" = Werkstatt baut · "ide" = vorbereiten für Zed
|
welt: str = "box" # "box" = Werkstatt baut · "ide" = vorbereiten für Agentic IDE
|
||||||
tiefe: str = "" # nur ide: "simpel" | "gruendlich" (leer = Box schätzt selbst)
|
tiefe: str = "" # nur ide: "simpel" | "gruendlich" (leer = Box schätzt selbst)
|
||||||
ziel: str = "" # nur ide-Projekt: "lxc" | "docker" (leer = Box entscheidet)
|
ziel: str = "" # nur ide-Projekt: "lxc" | "docker" (leer = Box entscheidet)
|
||||||
art: str = "projekt" # nur ide: "projekt" (Repo+Roadmap+Bau) | "idee" (nur durchdenken)
|
art: str = "projekt" # nur ide: "projekt" (Repo+Roadmap+Bau) | "idee" (nur durchdenken)
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
class RunSkillRequest(BaseModel):
|
||||||
|
skill_name: str
|
||||||
|
|
||||||
|
@router.get("/skills")
|
||||||
|
def list_skills():
|
||||||
|
"""Listet alle verfügbaren Skills aus dem deploy/skills Verzeichnis auf."""
|
||||||
|
# Pfad relativ zu dieser Datei (backend/routers/skills.py)
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
repo_root = os.path.dirname(os.path.dirname(current_dir))
|
||||||
|
skills_dir = os.path.join(repo_root, "deploy", "skills")
|
||||||
|
skills = []
|
||||||
|
|
||||||
|
# Check currently running hermes skill processes
|
||||||
|
running_skills = set()
|
||||||
|
for p in psutil.process_iter(['name', 'cmdline']):
|
||||||
|
try:
|
||||||
|
cmdline = p.info.get('cmdline')
|
||||||
|
if cmdline and any("hermes" in arg for arg in cmdline):
|
||||||
|
for arg in cmdline:
|
||||||
|
if "Führe den " in arg and " Skill aus" in arg:
|
||||||
|
# Extract skill name from "Führe den 'skill_name' Skill aus"
|
||||||
|
import re
|
||||||
|
match = re.search(r"Führe den '(.+?)' Skill aus", arg)
|
||||||
|
if match:
|
||||||
|
running_skills.add(match.group(1))
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
|
||||||
|
for entry in os.scandir(skills_dir):
|
||||||
|
if entry.is_dir():
|
||||||
|
# Suche nach SKILL.md für Metadaten
|
||||||
|
skill_md_path = os.path.join(entry.path, "SKILL.md")
|
||||||
|
description = ""
|
||||||
|
if os.path.exists(skill_md_path):
|
||||||
|
try:
|
||||||
|
with open(skill_md_path, "r", encoding="utf-8") as f:
|
||||||
|
# Lese erste Zeilen für Description (YAML Frontmatter)
|
||||||
|
lines = f.readlines()[:10]
|
||||||
|
for line in lines:
|
||||||
|
if "description:" in line.lower():
|
||||||
|
description = line.split(":", 1)[1].strip().strip('"\'')
|
||||||
|
break
|
||||||
|
elif "author:" in line.lower() and not description:
|
||||||
|
description = line.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
skills.append({
|
||||||
|
"name": entry.name,
|
||||||
|
"description": description or "Keine Beschreibung verfügbar.",
|
||||||
|
"running": entry.name in running_skills
|
||||||
|
})
|
||||||
|
return {"skills": skills}
|
||||||
|
|
||||||
|
@router.post("/skills/run")
|
||||||
|
def run_skill(req: RunSkillRequest):
|
||||||
|
"""Startet einen autonomen Skill via Hermes im Hintergrund."""
|
||||||
|
hermes_bin = os.path.expanduser("~/.local/bin/hermes")
|
||||||
|
if not os.path.exists(hermes_bin):
|
||||||
|
# Fallback falls lokal (auf Windows) entwickelt wird
|
||||||
|
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
|
||||||
|
|
||||||
|
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat
|
||||||
|
cmd = [hermes_bin, "-z", f"Führe den '{req.skill_name}' Skill aus", "chat"]
|
||||||
|
try:
|
||||||
|
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{req.skill_name.replace('/', '_')}.log")
|
||||||
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
||||||
|
with open(log_path, "a") as f:
|
||||||
|
f.write(f"\n--- Starting Skill: {req.skill_name} ---\n")
|
||||||
|
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
|
||||||
|
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "err": str(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
|
||||||
@@ -95,12 +88,10 @@ def services() -> dict:
|
|||||||
rows += [
|
rows += [
|
||||||
{"name": "Wächter (Steward)", "unit": "mc2-steward", "url": "",
|
{"name": "Wächter (Steward)", "unit": "mc2-steward", "url": "",
|
||||||
"ok": _user_unit_active("mc2-steward"), "scope": "user"},
|
"ok": _user_unit_active("mc2-steward"), "scope": "user"},
|
||||||
{"name": "Hermes-Gateway", "unit": "hermes-gateway", "url": HERMES_API_URL,
|
{"name": "Hermes-Gateway (Agent & Gedächtnis)", "unit": "hermes-gateway", "url": HERMES_API_URL,
|
||||||
"ok": a["gateway_reachable"], "scope": "user"},
|
"ok": a["gateway_reachable"], "scope": "user"},
|
||||||
{"name": "Hermes-GUI (Desktop-Gateway)", "unit": "hermes-builtin-ui", "url": a["hermes_ui_url"],
|
{"name": "Hermes-GUI (Desktop-Gateway)", "unit": "hermes-builtin-ui", "url": a["hermes_ui_url"],
|
||||||
"ok": a["hermes_ui_reachable"], "scope": "user"},
|
"ok": a["hermes_ui_reachable"], "scope": "user"},
|
||||||
{"name": "Mem0 (Gedächtnis)", "unit": "mem0-service", "url": MEM0_SERVICE_URL,
|
|
||||||
"ok": _mem0_reachable(), "scope": "user"},
|
|
||||||
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL,
|
{"name": "Voice (STT/TTS)", "unit": "voice-service", "url": VOICE_SERVICE_URL,
|
||||||
"ok": _voice_reachable(), "scope": "user"},
|
"ok": _voice_reachable(), "scope": "user"},
|
||||||
# ttyd hinter dem /console/-Proxy — war als einziger UI-Dienst NICHT in der Liste,
|
# ttyd hinter dem /console/-Proxy — war als einziger UI-Dienst NICHT in der Liste,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Voice-Endpoints für „Mit Hermes reden" (Browser-Voice + 3D-Avatar).
|
|||||||
|
|
||||||
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
Dünner Layer: STT/TTS werden zum Voice-Sidecar (:8650) geproxyt; der Chat geht an den
|
||||||
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
Hermes-`api_server` (:8642, OpenAI-kompatibel) — denselben vollen Agenten mit Tools +
|
||||||
geteiltem Mem0 wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
geteiltem Gedächtnis wie CLI/Telegram. Mit stabilem `X-Hermes-Session-Id` hält die Plattform den
|
||||||
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
Transcript server-seitig, daher schickt der Client je Turn nur die neue User-Nachricht.
|
||||||
|
|
||||||
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints.
|
||||||
@@ -169,7 +169,7 @@ def voice_metrics() -> dict:
|
|||||||
@router.get("/voice/trace")
|
@router.get("/voice/trace")
|
||||||
def voice_trace(limit: int = 20) -> dict:
|
def voice_trace(limit: int = 20) -> dict:
|
||||||
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
"""Per-Turn-Trace: die letzten `limit` Chat-Turns mit Stufen-Breakdown (STT · Vision · Hirn ·
|
||||||
Generierung, Mem0 als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
Generierung, Gedächtnis als Unter-Detail). Neueste zuerst. Für die Latenz-Ansicht im Cockpit —
|
||||||
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
damit man den EINEN langsamen Turn sieht, den ein Durchschnitt verschluckt."""
|
||||||
return {"turns": get_trace(limit)}
|
return {"turns": get_trace(limit)}
|
||||||
|
|
||||||
@@ -289,7 +289,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
headers["X-Hermes-Session-Key"] = body.session_key
|
headers["X-Hermes-Session-Key"] = body.session_key
|
||||||
|
|
||||||
async def gen():
|
async def gen():
|
||||||
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Mem0
|
# Per-Turn-Trace: sammelt STT (davor, geparkt) + Vision + Hirn-TTFT + Generierung + Gedächtnis
|
||||||
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
# (Rückruf während) zu EINEM Datensatz -> die Latenz-Ansicht zeigt den einzelnen Hänger.
|
||||||
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
trace = TurnTrace(session_id=body.session_id, kind="voice")
|
||||||
first = True
|
first = True
|
||||||
@@ -325,7 +325,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Gedächtnis-Extraktion.
|
||||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||||
try:
|
try:
|
||||||
@@ -342,7 +342,7 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
if first: # Time-To-First-Byte des Hermes-Streams (Verbindungs-Overhead)
|
||||||
trace.note_ttfb()
|
trace.note_ttfb()
|
||||||
first = False
|
first = False
|
||||||
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT) —
|
# Erster CONTENT-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT) —
|
||||||
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
# chat_ttfb misst nur den SSE-Start (~5 ms) und ist dafür blind.
|
||||||
if first_content and b'"content"' in chunk:
|
if first_content and b'"content"' in chunk:
|
||||||
trace.note_first_content()
|
trace.note_first_content()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom T
|
|||||||
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ def list_vault() -> dict:
|
|||||||
files = []
|
files = []
|
||||||
now = time.time()
|
now = time.time()
|
||||||
for p in sorted(VAULT.rglob("*.md")):
|
for p in sorted(VAULT.rglob("*.md")):
|
||||||
if ".git" in p.parts:
|
if ".git" in p.parts or "traeume" in p.parts:
|
||||||
continue
|
continue
|
||||||
rel = p.relative_to(VAULT).as_posix()
|
rel = p.relative_to(VAULT).as_posix()
|
||||||
st = p.stat()
|
st = p.stat()
|
||||||
@@ -90,3 +91,61 @@ def read_file(pfad: str) -> dict:
|
|||||||
raise
|
raise
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
raise HTTPException(404, "Notiz nicht lesbar.")
|
raise HTTPException(404, "Notiz nicht lesbar.")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/wissen/graph")
|
||||||
|
def graph_vault() -> dict:
|
||||||
|
"""Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links)."""
|
||||||
|
if not _available():
|
||||||
|
return {"nodes": [], "edges": []}
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
edges = []
|
||||||
|
# Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID)
|
||||||
|
name_to_id = {}
|
||||||
|
|
||||||
|
# 1. Alle Nodes sammeln
|
||||||
|
for p in VAULT.rglob("*.md"):
|
||||||
|
if ".git" in p.parts or "traeume" in p.parts:
|
||||||
|
continue
|
||||||
|
rel = p.relative_to(VAULT).as_posix()
|
||||||
|
name_to_id[p.stem.lower()] = rel
|
||||||
|
|
||||||
|
# Kategorie aus dem Ordner ableiten
|
||||||
|
cat = "knowledge"
|
||||||
|
parent = p.parent.name if p.parent != VAULT else ""
|
||||||
|
if parent == "traeume":
|
||||||
|
cat = "events"
|
||||||
|
elif parent == "muster":
|
||||||
|
cat = "rules"
|
||||||
|
elif parent == "skill-kandidaten":
|
||||||
|
cat = "identity"
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"id": rel,
|
||||||
|
"content": p.stem, # stem ist oft kürzer/prägnanter als der Titel
|
||||||
|
"category": cat,
|
||||||
|
"source": "wiki"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Edges extrahieren (Wiki-Links [[Name]])
|
||||||
|
link_rx = re.compile(r"\[\[([^\]]+)\]\]")
|
||||||
|
for n in nodes:
|
||||||
|
try:
|
||||||
|
target = VAULT / n["id"]
|
||||||
|
content = target.read_text(encoding="utf-8", errors="replace")
|
||||||
|
# Set, um mehrfache Links auf dieselbe Datei zu entduplizieren
|
||||||
|
found_links = {m.group(1).strip().lower() for m in link_rx.finditer(content)}
|
||||||
|
|
||||||
|
for link in found_links:
|
||||||
|
target_id = name_to_id.get(link)
|
||||||
|
if target_id and target_id != n["id"]:
|
||||||
|
edges.append({
|
||||||
|
"source": n["id"],
|
||||||
|
"target": target_id,
|
||||||
|
"weight": 1.0
|
||||||
|
})
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"nodes": nodes, "edges": edges}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from config import MODELS_DIR
|
from config import MODELS_DIR
|
||||||
@@ -60,6 +61,64 @@ _KANDIDAT_RX = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._ -]{0,120}\.md$")
|
|||||||
|
|
||||||
_fetch_cache: dict = {}
|
_fetch_cache: dict = {}
|
||||||
_FETCH_EVERY = 30.0 # s — Gitea nicht bei jedem UI-Poll anfragen
|
_FETCH_EVERY = 30.0 # s — Gitea nicht bei jedem UI-Poll anfragen
|
||||||
|
_CI_CACHE: dict = {} # { (repo, branch, head_sha): {"status": "success", "ts": time} }
|
||||||
|
|
||||||
|
def _gitea_creds() -> tuple | None:
|
||||||
|
# 1. Fallback: Versuch via git credential manager (zukunftsfähig & plattformübergreifend)
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
p = subprocess.run(
|
||||||
|
["git", "credential", "fill"],
|
||||||
|
input=b"protocol=https\nhost=git.tobisniceshomelab.ddnsfree.com\n",
|
||||||
|
capture_output=True,
|
||||||
|
check=True
|
||||||
|
)
|
||||||
|
out = p.stdout.decode(errors="replace")
|
||||||
|
u_match = re.search(r"username=(.+)", out)
|
||||||
|
p_match = re.search(r"password=(.+)", out)
|
||||||
|
if u_match and p_match:
|
||||||
|
return u_match.group(1).strip(), p_match.group(1).strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2. Fallback: Datei (hauptsächlich auf der AI-Box genutzt)
|
||||||
|
try:
|
||||||
|
cred = Path("~/.git-credentials").expanduser()
|
||||||
|
for line in cred.read_text(encoding="utf-8").splitlines():
|
||||||
|
m = re.match(r"https://([^:]+):([^@]+)@git\.tobisniceshomelab", line.strip())
|
||||||
|
if m: return m.group(1), m.group(2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _fetch_ci_status(repo: str, branch: str, head_sha: str) -> str | None:
|
||||||
|
# Nur auf der Box (wo creds liegen) sinnvoll
|
||||||
|
creds = _gitea_creds()
|
||||||
|
if not creds: return None
|
||||||
|
_user, token = creds
|
||||||
|
cache_key = (repo, branch, head_sha)
|
||||||
|
cached = _CI_CACHE.get(cache_key)
|
||||||
|
if cached and (time.time() - cached["ts"] < 30 or cached["status"] in ("success", "failure", "skipped")):
|
||||||
|
return cached["status"]
|
||||||
|
|
||||||
|
full_repo = "Hitonabi/mission-control-v2" if repo == "mc2" else "Hitonabi/lucy"
|
||||||
|
url = f"http://192.168.178.153:3000/api/v1/repos/{full_repo}/actions/runs?branch={urllib.parse.quote(branch)}&limit=1"
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, headers={"Authorization": "token " + token})
|
||||||
|
with urllib.request.urlopen(req, timeout=5) as r:
|
||||||
|
data = json.load(r)
|
||||||
|
runs = data if isinstance(data, list) else data.get("runs", data.get("workflow_runs", []))
|
||||||
|
if runs:
|
||||||
|
run = runs[0]
|
||||||
|
if run.get("head_sha", "").startswith(head_sha[:7]):
|
||||||
|
st = run.get("status")
|
||||||
|
if st:
|
||||||
|
_CI_CACHE[cache_key] = {"status": st, "ts": time.time()}
|
||||||
|
return st
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("auftragsbuch: CI-Status fetch fehler: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _available() -> bool:
|
def _available() -> bool:
|
||||||
@@ -176,6 +235,10 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
|
|||||||
stempel = gutachten.get(f"{repo}:{branch}")
|
stempel = gutachten.get(f"{repo}:{branch}")
|
||||||
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
|
if not (isinstance(stempel, dict) and stempel.get("commit_ts") == ts_val):
|
||||||
stempel = None
|
stempel = None
|
||||||
|
|
||||||
|
head_sha = (_git(repo, ["rev-parse", ref]).stdout or "").strip()
|
||||||
|
ci_status = _fetch_ci_status(repo, branch, head_sha) if head_sha else None
|
||||||
|
|
||||||
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
|
stat = (_git(repo, ["diff", "--shortstat", f"origin/main...{ref}"]).stdout or "").strip()
|
||||||
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
|
files_raw = (_git(repo, ["diff", "--name-status", f"origin/main...{ref}"]).stdout or "").splitlines()
|
||||||
files = []
|
files = []
|
||||||
@@ -209,6 +272,7 @@ def _repo_items(repo: str, statuses: dict, gutachten: dict) -> list[dict]:
|
|||||||
# es im Repo liegt. Lucy wird dagegen IMMER am PC gebaut (kein dist im Repo).
|
# es im Repo liegt. Lucy wird dagegen IMMER am PC gebaut (kein dist im Repo).
|
||||||
"frontend_ohne_build": repo == "mc2" and frontend_src and not frontend_dist,
|
"frontend_ohne_build": repo == "mc2" and frontend_src and not frontend_dist,
|
||||||
"status": None if (status or {}).get("state") == "eingespielt" and ahead > 0 else status,
|
"status": None if (status or {}).get("state") == "eingespielt" and ahead > 0 else status,
|
||||||
|
"ci_status": ci_status,
|
||||||
})
|
})
|
||||||
except Exception:
|
except Exception:
|
||||||
log.warning("auftragsbuch: Branch %s (%s) nicht lesbar", branch, repo, exc_info=True)
|
log.warning("auftragsbuch: Branch %s (%s) nicht lesbar", branch, repo, exc_info=True)
|
||||||
|
|||||||
@@ -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,7 +10,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"
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ def build_snippets(host: str = DEFAULT_HOST,
|
|||||||
def check_health() -> dict:
|
def check_health() -> dict:
|
||||||
"""Live-Erreichbarkeit der drei Leitungen, aus Sicht der Box:
|
"""Live-Erreichbarkeit der drei Leitungen, aus Sicht der Box:
|
||||||
Leitung 1 = der ECHTE Modell-Pfad, den Clients nutzen (mc2-gateway bei V1_UPSTREAM,
|
Leitung 1 = der ECHTE Modell-Pfad, den Clients nutzen (mc2-gateway bei V1_UPSTREAM,
|
||||||
sonst llama-swap direkt), Leitung 2 = Gedächtnis-Sidecar (Mem0),
|
sonst llama-swap direkt), Leitung 2 = Natives Hermes-Gedächtnis (hermes-gateway),
|
||||||
Leitung 3 = Desktop-Gateway (Hermes-Builtin-UI)."""
|
Leitung 3 = Desktop-Gateway (Hermes-Builtin-UI)."""
|
||||||
gateway = {"ok": False, "detail": "nicht erreichbar"}
|
gateway = {"ok": False, "detail": "nicht erreichbar"}
|
||||||
try:
|
try:
|
||||||
@@ -144,9 +144,12 @@ def check_health() -> dict:
|
|||||||
memory = {"ok": False, "detail": "nicht erreichbar"}
|
memory = {"ok": False, "detail": "nicht erreichbar"}
|
||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=3.0) as c:
|
with httpx.Client(timeout=3.0) as c:
|
||||||
r = c.get(f"{MEM0_SERVICE_URL}/health")
|
# Hermes Gateway stellt das native Gedächtnis & Agent-Loop bereit
|
||||||
memory = ({"ok": True, "detail": "bereit"} if r.status_code == 200
|
r = c.get("http://127.0.0.1:8642/health")
|
||||||
else {"ok": False, "detail": f"HTTP {r.status_code}"})
|
if r.status_code in (200, 404, 401):
|
||||||
|
memory = {"ok": True, "detail": "Hermes-Nativ bereit"}
|
||||||
|
else:
|
||||||
|
memory = {"ok": False, "detail": f"HTTP {r.status_code}"}
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -392,12 +392,12 @@ def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui",
|
|||||||
welt='ide': geht DIREKT an das Profil `projektstart` (am Triage/Auto-Zerleger vorbei) —
|
welt='ide': geht DIREKT an das Profil `projektstart` (am Triage/Auto-Zerleger vorbei) —
|
||||||
die Box härtet ein Konzept (konzept-fliessband), legt ein leeres Gitea-Repo an und
|
die Box härtet ein Konzept (konzept-fliessband), legt ein leeres Gitea-Repo an und
|
||||||
füllt es NUR mit Doku. Ergebnis ist ein vorbereitetes Projekt, das der Commander in
|
füllt es NUR mit Doku. Ergebnis ist ein vorbereitetes Projekt, das der Commander in
|
||||||
Zed selbst baut (IDE-Welt). Bewusst KEIN --triage: so fasst der Auto-Zerleger die
|
Agentic IDE selbst baut (IDE-Welt). Bewusst KEIN --triage: so fasst der Auto-Zerleger die
|
||||||
Karte nie an und die Werkstatt baut sie nie (der Fan-out-Fehler vom 20.07.).
|
Karte nie an und die Werkstatt baut sie nie (der Fan-out-Fehler vom 20.07.).
|
||||||
tiefe (nur ide): 'simpel' | 'gruendlich' — vom Knopf gewaehlte Konzept-Tiefe; leer =
|
tiefe (nur ide): 'simpel' | 'gruendlich' — vom Knopf gewaehlte Konzept-Tiefe; leer =
|
||||||
die Box schaetzt selbst (Fallback fuer Telegram/Lucy).
|
die Box schaetzt selbst (Fallback fuer Telegram/Lucy).
|
||||||
ziel (nur ide-Projekt): 'lxc' | 'docker' — Ziel-Plattform; leer = die Box entscheidet.
|
ziel (nur ide-Projekt): 'lxc' | 'docker' — Ziel-Plattform; leer = die Box entscheidet.
|
||||||
art (nur ide): 'projekt' (Default, Repo+Doku+Roadmap, Bau in Zed) oder 'idee'
|
art (nur ide): 'projekt' (Default, Repo+Doku+Roadmap, Bau in IDE) oder 'idee'
|
||||||
(Produkt-Idee nur durchdenken: NUR Konzept, keine Roadmap/Geruest, kein Bau-Druck)."""
|
(Produkt-Idee nur durchdenken: NUR Konzept, keine Roadmap/Geruest, kein Bau-Druck)."""
|
||||||
if not _available():
|
if not _available():
|
||||||
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
|
return {"ok": False, "error": "Die Ideen-Queue lebt auf der Box."}
|
||||||
@@ -456,7 +456,7 @@ def add_idea(titel: str, notiz: str = "", created_by: str = "mc2-ui",
|
|||||||
"[Ideen-Queue]", "ideen-queue", "silent")
|
"[Ideen-Queue]", "ideen-queue", "silent")
|
||||||
elif welt == "ide":
|
elif welt == "ide":
|
||||||
announce.add(f"Neues IDE-Projekt in Vorbereitung: „{titel}“ ({task['id']}) — die Box "
|
announce.add(f"Neues IDE-Projekt in Vorbereitung: „{titel}“ ({task['id']}) — die Box "
|
||||||
"härtet das Konzept und legt das Repo an, dann baust du in Zed.",
|
"härtet das Konzept und legt das Repo an, dann baust du in der Agentic IDE.",
|
||||||
"[Ideen-Queue]", "ideen-queue", "silent")
|
"[Ideen-Queue]", "ideen-queue", "silent")
|
||||||
else:
|
else:
|
||||||
announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
|
announce.add(f"Neue Idee in der Queue: „{titel}“ ({task['id']}) — die Box arbeitet sie aus.",
|
||||||
@@ -515,10 +515,10 @@ def archive_task(task_id: str) -> dict:
|
|||||||
_EINORDNEN_SYS = (
|
_EINORDNEN_SYS = (
|
||||||
"Du ordnest eine frisch getippte Projekt-Idee ein. Antworte AUSSCHLIESSLICH mit einem "
|
"Du ordnest eine frisch getippte Projekt-Idee ein. Antworte AUSSCHLIESSLICH mit einem "
|
||||||
"JSON-Objekt, ohne Prosa, ohne Codezaun:\n"
|
"JSON-Objekt, ohne Prosa, ohne Codezaun:\n"
|
||||||
'{"wer":"box"|"zed","tiefe":"schlank"|"gruendlich","nur_denken":true|false}\n\n'
|
'{"wer":"box"|"ide","tiefe":"schlank"|"gruendlich","nur_denken":true|false}\n\n'
|
||||||
"wer=box: Aufgabe am eigenen Heimlabor des Nutzers — seine Server, seine bestehenden "
|
"wer=box: Aufgabe am eigenen Heimlabor des Nutzers — seine Server, seine bestehenden "
|
||||||
"Dienste (Mission Control, Lucy, Rippy), Wartung, Doku, Automatisierung. Die Box baut selbst.\n"
|
"Dienste (Mission Control, Lucy, Rippy), Wartung, Doku, Automatisierung. Die Box baut selbst.\n"
|
||||||
"wer=zed: ein NEUES eigenstaendiges Programm/Werkzeug/App, das der Nutzer danach selbst "
|
"wer=ide: ein NEUES eigenstaendiges Programm/Werkzeug/App, das der Nutzer danach selbst "
|
||||||
"programmieren will. Er bekommt nur ein vorbereitetes Repo.\n"
|
"programmieren will. Er bekommt nur ein vorbereitetes Repo.\n"
|
||||||
"tiefe=gruendlich: gross, vage oder mit vielen offenen Entwurfsfragen. "
|
"tiefe=gruendlich: gross, vage oder mit vielen offenen Entwurfsfragen. "
|
||||||
"tiefe=schlank: klein und klar umrissen.\n"
|
"tiefe=schlank: klein und klar umrissen.\n"
|
||||||
@@ -564,11 +564,11 @@ def einordnen(text: str) -> dict:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return {"ok": False}
|
return {"ok": False}
|
||||||
|
|
||||||
wer = d.get("wer") if d.get("wer") in ("box", "zed") else None
|
wer = d.get("wer") if d.get("wer") in ("box", "ide") else None
|
||||||
tiefe = d.get("tiefe") if d.get("tiefe") in ("schlank", "gruendlich") else None
|
tiefe = d.get("tiefe") if d.get("tiefe") in ("schlank", "gruendlich") else None
|
||||||
if wer is None and tiefe is None:
|
if wer is None and tiefe is None:
|
||||||
return {"ok": False}
|
return {"ok": False}
|
||||||
return {"ok": True, "wer": wer or "zed", "tiefe": tiefe or "schlank",
|
return {"ok": True, "wer": wer or "ide", "tiefe": tiefe or "schlank",
|
||||||
"nur_denken": bool(d.get("nur_denken"))}
|
"nur_denken": bool(d.get("nur_denken"))}
|
||||||
|
|
||||||
|
|
||||||
@@ -981,7 +981,7 @@ def konzept_of(task_id: str) -> dict:
|
|||||||
_AUS_IDEE_KOPF = (
|
_AUS_IDEE_KOPF = (
|
||||||
"AUFTRAGS-ART: PROJEKT AUS GEPRUEFTER IDEE. Der Commander hat diese Idee erst durchdenken "
|
"AUFTRAGS-ART: PROJEKT AUS GEPRUEFTER IDEE. Der Commander hat diese Idee erst durchdenken "
|
||||||
"lassen — das Konzept gefaellt ihm, jetzt soll daraus ein startklares Repo werden, das er "
|
"lassen — das Konzept gefaellt ihm, jetzt soll daraus ein startklares Repo werden, das er "
|
||||||
"SELBST in Zed baut.")
|
"SELBST in der Agentic IDE baut.")
|
||||||
_AUS_IDEE_FUSS = (
|
_AUS_IDEE_FUSS = (
|
||||||
"· Denk das Konzept NICHT neu. Es ist bereits mit voller Kaskade + Advocatus Diaboli "
|
"· Denk das Konzept NICHT neu. Es ist bereits mit voller Kaskade + Advocatus Diaboli "
|
||||||
"gehaertet. Lies es vollstaendig und uebernimm es; eine kurze Nachschaerf-Runde fuer die "
|
"gehaertet. Lies es vollstaendig und uebernimm es; eine kurze Nachschaerf-Runde fuer die "
|
||||||
@@ -989,7 +989,7 @@ _AUS_IDEE_FUSS = (
|
|||||||
"Verschwendung.\n"
|
"Verschwendung.\n"
|
||||||
"· Der Rest ist deine normale Projektstart-Rolle: das Repo enthaelt am Ende NUR Doku "
|
"· Der Rest ist deine normale Projektstart-Rolle: das Repo enthaelt am Ende NUR Doku "
|
||||||
"(KONZEPT.md, ROADMAP.md, AGENTS.md, README.md, SAVEPOINT.md, .aiexclude) — KEIN Code.\n"
|
"(KONZEPT.md, ROADMAP.md, AGENTS.md, README.md, SAVEPOINT.md, .aiexclude) — KEIN Code.\n"
|
||||||
"· Uebergabe wie ueblich: Repo-URL nennen, damit der Commander in Zed loslegen kann.")
|
"· Uebergabe wie ueblich: Repo-URL nennen, damit der Commander in der Agentic IDE loslegen kann.")
|
||||||
|
|
||||||
|
|
||||||
def _wo_liegt(konz: dict) -> str:
|
def _wo_liegt(konz: dict) -> str:
|
||||||
@@ -1090,7 +1090,7 @@ def projekt_aus_idee(task_id: str, ziel: str = "", tiefe: str = "") -> dict:
|
|||||||
try:
|
try:
|
||||||
from services import announce
|
from services import announce
|
||||||
announce.add(f"Aus der Idee „{_kurz(kurz, 60)}“ wird ein Projekt: {task['id']} — die Box "
|
announce.add(f"Aus der Idee „{_kurz(kurz, 60)}“ wird ein Projekt: {task['id']} — die Box "
|
||||||
"bereitet das Repo vor, gebaut wird in Zed.",
|
"bereitet das Repo vor, gebaut wird in der Agentic IDE.",
|
||||||
"[Ideen-Queue]", "ideen-queue", "silent")
|
"[Ideen-Queue]", "ideen-queue", "silent")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
"""
|
|
||||||
Ketten-Digest — Telegram-Status für lange Aufgaben-Ketten (User-Wunsch 20.07.2026).
|
|
||||||
|
|
||||||
Wenn die Werkstatt eine Karten-Familie abarbeitet (z. B. „Homelab-Dashboard 2.0" mit
|
|
||||||
19 verketteten Karten), soll der Commander nicht raten müssen: dieser Wächter liest
|
|
||||||
die Queue-Sicht (services/ideen.py, inkl. Ketten-Anreicherung) und meldet per Telegram
|
|
||||||
|
|
||||||
• SOFORT, wenn eine Karte hängt und eine Frage an den Commander hat (blocked) —
|
|
||||||
einmal pro Karte, mit der Frage im Wortlaut,
|
|
||||||
• bei MEILENSTEINEN (Projekt komplett fertig) sofort,
|
|
||||||
• sonst höchstens einmal pro PULS_SEKUNDEN (Default 60 min) einen Zwischenstand
|
|
||||||
je aktivem Projekt („5/19 fertig · läuft: … · als Nächstes: …").
|
|
||||||
|
|
||||||
Taktung war explizite User-Wahl („Meilensteine + 60-min-Puls, Fragen immer sofort").
|
|
||||||
Zustand (was wurde wann gemeldet) liegt als JSON neben den Modellen und übersteht
|
|
||||||
Neustarts — sonst käme nach jedem Deploy ein Duplikat-Schwall. Auf Windows (Dev) No-op.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import zlib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from config import MODELS_DIR
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
ENABLED = os.name == "posix" and os.environ.get("MC_KETTEN_DIGEST", "1") != "0"
|
|
||||||
INTERVAL = int(os.environ.get("MC_KETTEN_DIGEST_INTERVAL", "300")) # Prüf-Tick: 5 min
|
|
||||||
PULS_SEKUNDEN = int(os.environ.get("MC_KETTEN_DIGEST_PULS", "3600")) # Zwischenstand: 60 min
|
|
||||||
# Hänger-Alarm: laufende Karte ohne Lebenszeichen (Heartbeat) länger als diese Schwelle
|
|
||||||
# → sofortige Telegram-Warnung (einmal pro Karten-Lauf). Live-Fall 20.07.: Worker wartete
|
|
||||||
# 45 min auf einen selbst gestarteten uvicorn — „pid_alive" hielt den Claim ewig frisch.
|
|
||||||
HANG_SEKUNDEN = int(os.environ.get("MC_KETTEN_HANG", "900"))
|
|
||||||
KANBAN_LOGS = Path(os.environ.get("MC_KANBAN_LOGS", "~/.hermes/kanban/logs")).expanduser()
|
|
||||||
STATE_PATH = Path(os.environ.get("MC_KETTEN_DIGEST_STATE", str(MODELS_DIR / "mc2-ketten-digest.json")))
|
|
||||||
|
|
||||||
|
|
||||||
def _kontext_druck(task_id: str) -> int:
|
|
||||||
"""Wie oft der Worker zuletzt den Kontext verdichten musste (Log-Marker) —
|
|
||||||
mehrfaches „Compacting context" heißt: die Aufgabe sprengt das Fenster."""
|
|
||||||
try:
|
|
||||||
p = KANBAN_LOGS / f"{task_id}.log"
|
|
||||||
with p.open("rb") as f:
|
|
||||||
f.seek(max(0, p.stat().st_size - 200_000))
|
|
||||||
tail = f.read().decode("utf-8", errors="replace")
|
|
||||||
return tail.count("Compacting context")
|
|
||||||
except Exception:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def _load_state() -> dict:
|
|
||||||
try:
|
|
||||||
d = json.loads(STATE_PATH.read_text(encoding="utf-8"))
|
|
||||||
return d if isinstance(d, dict) else {}
|
|
||||||
except Exception:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
def _save_state(state: dict) -> None:
|
|
||||||
try:
|
|
||||||
tmp = STATE_PATH.with_suffix(".tmp")
|
|
||||||
tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
|
|
||||||
tmp.replace(STATE_PATH)
|
|
||||||
except OSError:
|
|
||||||
log.warning("ketten-digest: Zustand %s nicht schreibbar", STATE_PATH, exc_info=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _melden(subject: str, text: str) -> None:
|
|
||||||
"""Aufs Handy UND in den Briefkasten (silent — Lucy muss den Status nicht sprechen)."""
|
|
||||||
from services import announce
|
|
||||||
announce.notify_telegram(subject, text)
|
|
||||||
try:
|
|
||||||
announce.add(text, subject, "ketten-digest", "silent")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _tick() -> None:
|
|
||||||
from services import ideen
|
|
||||||
|
|
||||||
data = ideen.list_queue()
|
|
||||||
if not data.get("available"):
|
|
||||||
return
|
|
||||||
items = data.get("items") or []
|
|
||||||
projekte = data.get("projekte") or []
|
|
||||||
state = _load_state()
|
|
||||||
gemeldete_fragen: dict = state.setdefault("fragen", {})
|
|
||||||
projekt_state: dict = state.setdefault("projekte", {})
|
|
||||||
dirty = False
|
|
||||||
now = int(time.time())
|
|
||||||
|
|
||||||
# 1) Hängende Karten mit Frage → sofort, einmal pro Karte. Der Schlüssel enthält den
|
|
||||||
# Fragen-Text-Hash: hängt dieselbe Karte später mit NEUER Frage, wird wieder gemeldet.
|
|
||||||
for it in items:
|
|
||||||
if it.get("status") != "blocked":
|
|
||||||
continue
|
|
||||||
frage = (it.get("frage") or "").strip()
|
|
||||||
key = f"{it['id']}:{zlib.crc32(frage.encode('utf-8')):x}"
|
|
||||||
if key in gemeldete_fragen:
|
|
||||||
continue
|
|
||||||
text = (f"Aufgabe hängt und wartet auf dich: „{(it.get('titel') or '')[:120]}“\n"
|
|
||||||
+ (f"Frage: {frage[:500]}\n" if frage else "")
|
|
||||||
+ "Antworten geht im Auftragsbuch (Zentrale) — die Box macht dann weiter.")
|
|
||||||
_melden("[Werkstatt]", text)
|
|
||||||
gemeldete_fragen[key] = now
|
|
||||||
dirty = True
|
|
||||||
|
|
||||||
# 1b) Hänger-Alarm: laufende Karte ohne Lebenszeichen → einmal pro Karten-Lauf warnen.
|
|
||||||
gemeldete_haenger: dict = state.setdefault("haenger", {})
|
|
||||||
for it in items:
|
|
||||||
if it.get("status") != "running":
|
|
||||||
continue
|
|
||||||
puls = it.get("puls_alter")
|
|
||||||
if puls is None or puls < HANG_SEKUNDEN:
|
|
||||||
continue
|
|
||||||
key = f"{it['id']}:{it.get('gestartet') or 0}"
|
|
||||||
if key in gemeldete_haenger:
|
|
||||||
continue
|
|
||||||
minuten = int(puls / 60)
|
|
||||||
text = (f"Karte scheint zu hängen: „{(it.get('titel') or '')[:120]}“ — "
|
|
||||||
f"seit {minuten} min kein Lebenszeichen vom Worker.")
|
|
||||||
druck = _kontext_druck(it["id"])
|
|
||||||
if druck >= 2:
|
|
||||||
text += (f"\nDer Worker musste {druck}× den Kontext verdichten — "
|
|
||||||
"die Aufgabe ist womöglich zu groß geschnitten.")
|
|
||||||
text += "\nIn der Zentrale: „Neuer Versuch“ startet sie frisch, „Stopp“ hält sie an."
|
|
||||||
_melden("[Werkstatt]", text)
|
|
||||||
gemeldete_haenger[key] = now
|
|
||||||
dirty = True
|
|
||||||
if len(gemeldete_haenger) > 200:
|
|
||||||
for k in sorted(gemeldete_haenger, key=gemeldete_haenger.get)[:100]:
|
|
||||||
gemeldete_haenger.pop(k, None)
|
|
||||||
dirty = True
|
|
||||||
|
|
||||||
# Fragen-Gedächtnis klein halten (Karten verschwinden irgendwann ins Archiv).
|
|
||||||
if len(gemeldete_fragen) > 200:
|
|
||||||
for k in sorted(gemeldete_fragen, key=gemeldete_fragen.get)[:100]:
|
|
||||||
gemeldete_fragen.pop(k, None)
|
|
||||||
dirty = True
|
|
||||||
|
|
||||||
# 2) Projekt-Status: Abschluss sofort, sonst gedrosselter Puls solange gearbeitet wird.
|
|
||||||
for p in projekte:
|
|
||||||
ps = projekt_state.setdefault(p["key"], {})
|
|
||||||
fertig, gesamt = p.get("fertig", 0), p.get("gesamt", 0)
|
|
||||||
|
|
||||||
if gesamt > 0 and fertig >= gesamt and not ps.get("abschluss_gemeldet"):
|
|
||||||
_melden("[Werkstatt]", f"Projekt fertig: „{p['titel']}“ — alle {gesamt} Karten erledigt. "
|
|
||||||
"Ergebnisse liegen im Auftragsbuch.")
|
|
||||||
ps.update(abschluss_gemeldet=True, letzter_puls=now, letzter_stand=fertig)
|
|
||||||
dirty = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
aktiv = p.get("laeuft", 0) > 0
|
|
||||||
if not aktiv:
|
|
||||||
continue
|
|
||||||
if now - int(ps.get("letzter_puls") or 0) < PULS_SEKUNDEN:
|
|
||||||
continue
|
|
||||||
laufende = [i for i in items if i.get("projekt") == p["key"] and i["status"] == "running"]
|
|
||||||
naechste = [i for i in items if i.get("projekt") == p["key"]
|
|
||||||
and i["status"] in ("ready", "todo") and not i.get("wartet_auf")]
|
|
||||||
zeilen = [f"Werkstatt-Status „{p['titel']}“: {fertig}/{gesamt} fertig."]
|
|
||||||
for l in laufende[:2]:
|
|
||||||
note = f" — {l['notiz']}" if l.get("notiz") else ""
|
|
||||||
zeilen.append(f"Läuft: {l['titel'][:90]}{note}")
|
|
||||||
if naechste:
|
|
||||||
zeilen.append(f"Als Nächstes: {naechste[0]['titel'][:90]}")
|
|
||||||
haengt = p.get("haengt", 0)
|
|
||||||
if haengt:
|
|
||||||
zeilen.append(f"{haengt} Karte(n) warten auf deine Antwort im Auftragsbuch.")
|
|
||||||
_melden("[Werkstatt]", "\n".join(zeilen))
|
|
||||||
ps.update(letzter_puls=now, letzter_stand=fertig)
|
|
||||||
dirty = True
|
|
||||||
|
|
||||||
if dirty:
|
|
||||||
_save_state(state)
|
|
||||||
|
|
||||||
|
|
||||||
async def digest_loop() -> None:
|
|
||||||
"""Hintergrund-Task im MC2-Lifespan (Muster: reminders_loop)."""
|
|
||||||
log.info("ketten-digest: aktiv (Tick %ss, Puls %ss)", INTERVAL, PULS_SEKUNDEN)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
await asyncio.to_thread(_tick)
|
|
||||||
except Exception:
|
|
||||||
log.debug("ketten-digest: Tick fehlgeschlagen", exc_info=True)
|
|
||||||
await asyncio.sleep(INTERVAL)
|
|
||||||
@@ -20,7 +20,7 @@ from services import catalog, discover, jobengine, llamaswap, system
|
|||||||
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
# System-Dienste (root, via sudo -n NOPASSWD) vs. User-Dienste (systemctl --user).
|
||||||
SYSTEM_SERVICES = {"llama-swap"}
|
SYSTEM_SERVICES = {"llama-swap"}
|
||||||
USER_SERVICES = {"mission-control-2", "mc2-gateway", "mc2-steward", "box-console",
|
USER_SERVICES = {"mission-control-2", "mc2-gateway", "mc2-steward", "box-console",
|
||||||
"hermes-gateway", "hermes-builtin-ui", "mem0-service", "voice-service"}
|
"hermes-gateway", "hermes-builtin-ui", "voice-service"}
|
||||||
|
|
||||||
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
# Engine-Update: lädt den neuesten Vulkan-Build (deploy/update-engine.sh, läuft als root).
|
||||||
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
_REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
@@ -73,8 +73,9 @@ def _installed_engine_build() -> int | None:
|
|||||||
out = subprocess.run([bin_path, "--version"], capture_output=True, text=True,
|
out = subprocess.run([bin_path, "--version"], capture_output=True, text=True,
|
||||||
timeout=20, env=env)
|
timeout=20, env=env)
|
||||||
txt = (out.stderr or "") + (out.stdout or "")
|
txt = (out.stderr or "") + (out.stdout or "")
|
||||||
# Formate je nach Build: "version: 9821 (hash)" (aktuell), "build: <hash> (9821)", "b9821".
|
# Formate je nach Build: "version: 0.1.0-dev (build 10426, ...)", "version: 9821 (hash)", "build: <hash> (9821)", "b9821".
|
||||||
if (m := re.search(r"version:\s*(\d{3,})", txt)) \
|
if (m := re.search(r"\bbuild\s+(\d{3,})\b", txt)) \
|
||||||
|
or (m := re.search(r"version:\s*(\d{3,})", txt)) \
|
||||||
or (m := re.search(r"build:\s*\S+\s*\((\d+)\)", txt)) \
|
or (m := re.search(r"build:\s*\S+\s*\((\d+)\)", txt)) \
|
||||||
or (m := re.search(r"\bb(\d{3,})\b", txt)):
|
or (m := re.search(r"\bb(\d{3,})\b", txt)):
|
||||||
return int(m.group(1))
|
return int(m.group(1))
|
||||||
|
|||||||
@@ -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_URL, MODELS_DIR, VOICE_SERVICE_URL
|
||||||
|
|
||||||
from services import announce, llamaswap
|
from services import announce, llamaswap
|
||||||
|
|
||||||
@@ -70,12 +70,9 @@ 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),
|
"hermes": (lambda: _reach(HERMES_API_URL, "/v1/models"),
|
||||||
"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."),
|
||||||
"mem0": (lambda: _reach(MEM0_SERVICE_URL),
|
|
||||||
"Mein Gedächtnis-Dienst ist ausgefallen — ich merke mir vorübergehend nichts Neues.",
|
|
||||||
"Mein Gedächtnis ist wieder online."),
|
|
||||||
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
"voice": (lambda: _reach(VOICE_SERVICE_URL),
|
||||||
"Der Hör-Dienst auf der Box ist ausgefallen — Spracheingabe könnte haken.",
|
"Der Hör-Dienst auf der Box ist ausgefallen — Spracheingabe könnte haken.",
|
||||||
"Der Hör-Dienst läuft wieder."),
|
"Der Hör-Dienst läuft wieder."),
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ Zwei Sichten auf dieselben Messungen:
|
|||||||
|
|
||||||
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
**Was MC2 messen kann — und was nicht:** MC2 proxyt den Chat nur an Hermes (:8642). Die Stufen STT,
|
||||||
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
Vision, Hirn-TTFT (Zeit bis zum ersten Inhalts-Token) und Generierung sind hier direkt messbar. Der
|
||||||
**Mem0-Retrieve** läuft zwar in Hermes, ruft aber MC2s `/api/memory` per HTTP zurück → messbar und als
|
**Gedächtnis-Retrieve** läuft zwar in Hermes, ruft aber MC2s Memory-Endpoint per HTTP zurück → messbar
|
||||||
Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden** dagegen laufen
|
und als Unter-Detail INNERHALB der Hirn-Zeit ausgewiesen (kein Doppelzählen). Die **Tool-Runden**
|
||||||
im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im „Generierung"-Bucket.
|
dagegen laufen im Hermes-LLM-Loop ohne Callback an MC2 → für MC2 unsichtbar, sie stecken im
|
||||||
|
„Generierung"-Bucket.
|
||||||
|
|
||||||
STT (davor) und Mem0-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
STT (davor) und Gedächtnis-Retrieve (währenddessen) sind separate HTTP-Requests ohne Turn-ID. Auf einem
|
||||||
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
EIN-Nutzer-Gerät genügt eine schlanke Best-Effort-Korrelation: die zuletzt gemessene STT-Dauer bzw. der
|
||||||
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
letzte Retrieve werden global „geparkt" und vom nächsten Chat-Turn eingesammelt (mit Frist-/Reihenfolge-
|
||||||
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
Check). Kein Turn-ID-Durchreichen durch den Lucy-Client nötig.
|
||||||
@@ -36,7 +37,7 @@ _TURNS: deque = deque(maxlen=60) # letzte N vollständige Chat-Turns (Per-Turn-
|
|||||||
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
||||||
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
|
STAGES = ("stt", "vision", "memory_retrieve", "chat_ttfb", "chat_first_content", "tts")
|
||||||
|
|
||||||
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Mem0-Retrieve, je
|
# Best-effort-Korrelation (Ein-Nutzer-Gerät): zuletzt gemessene STT-Dauer / Gedächtnis-Retrieve, je
|
||||||
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
# (ms, perf_counter-Zeitstempel). Der nächste passende Chat-Turn sammelt sie ein und leert sie.
|
||||||
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
_PARKED: dict[str, tuple[float, float] | None] = {"stt": None, "retrieve": None}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ def record_stage(stage: str, ms: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def park(kind: str, ms: float) -> None:
|
def park(kind: str, ms: float) -> None:
|
||||||
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Mem0-Retrieve als
|
"""Eine Messung, die NICHT im Chat-Request selbst passiert (STT davor, Gedächtnis-Retrieve als
|
||||||
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
Rückruf während), global parken, damit der nächste Chat-Turn sie einsammeln kann."""
|
||||||
if ms is None or ms < 0 or kind not in _PARKED:
|
if ms is None or ms < 0 or kind not in _PARKED:
|
||||||
return
|
return
|
||||||
@@ -72,7 +73,7 @@ def _take_stt(max_age: float = 20.0) -> float | None:
|
|||||||
|
|
||||||
|
|
||||||
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
def _take_retrieve(since_perf: float, max_age: float = 90.0) -> float | None:
|
||||||
"""Geparkten Mem0-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
"""Geparkten Gedächtnis-Retrieve einsammeln, wenn er NACH dem Turn-Start kam (Rückruf während des
|
||||||
Turns) und frisch ist."""
|
Turns) und frisch ist."""
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
v = _PARKED.get("retrieve")
|
v = _PARKED.get("retrieve")
|
||||||
@@ -103,7 +104,7 @@ class TurnTrace:
|
|||||||
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
gehalten; `commit()` schreibt den Datensatz in den Ringpuffer UND speist die rollenden Stats.
|
||||||
|
|
||||||
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
Balken-Stufen (zeitlich DISJUNKT, ergeben zusammen den Turn): stt · vision · hirn · gen.
|
||||||
Unter-Detail: mem0 (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
Unter-Detail: memory (Teil VON hirn, wird separat ausgewiesen, aber NICHT zum Balken addiert)."""
|
||||||
|
|
||||||
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
def __init__(self, session_id: str = "", kind: str = "voice") -> None:
|
||||||
self.id = uuid.uuid4().hex[:8]
|
self.id = uuid.uuid4().hex[:8]
|
||||||
@@ -113,7 +114,7 @@ class TurnTrace:
|
|||||||
self.session_id = (session_id or "")[:24]
|
self.session_id = (session_id or "")[:24]
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
self.vision_ms: float | None = None # Bildschirm-Beschreibung (falls Bilder)
|
||||||
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Mem0 + TTFT)
|
self.hirn_ms: float | None = None # Zeit bis zum ersten Inhalts-Token (Agent + Gedächtnis + TTFT)
|
||||||
self.had_images = False
|
self.had_images = False
|
||||||
self.error: str | None = None
|
self.error: str | None = None
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ class TurnTrace:
|
|||||||
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
record_stage("chat_ttfb", (time.perf_counter() - self._brain0) * 1000.0) # SSE-Start (~5 ms), nur rollend
|
||||||
|
|
||||||
def note_first_content(self) -> None:
|
def note_first_content(self) -> None:
|
||||||
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Mem0 + LLM-TTFT)."""
|
"""Erster Inhalts-Delta = echte Hirn-Latenz (Agent-Overhead + Gedächtnis + LLM-TTFT)."""
|
||||||
ms = (time.perf_counter() - self._brain0) * 1000.0
|
ms = (time.perf_counter() - self._brain0) * 1000.0
|
||||||
self.hirn_ms = round(ms, 1)
|
self.hirn_ms = round(ms, 1)
|
||||||
record_stage("chat_first_content", ms)
|
record_stage("chat_first_content", ms)
|
||||||
@@ -138,7 +139,7 @@ class TurnTrace:
|
|||||||
def commit(self) -> dict:
|
def commit(self) -> dict:
|
||||||
total = (time.perf_counter() - self.perf0) * 1000.0
|
total = (time.perf_counter() - self.perf0) * 1000.0
|
||||||
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
stt = _take_stt() # rollend bereits in /voice/stt erfasst
|
||||||
mem0 = _take_retrieve(self.perf0) # rollend bereits in /api/memory erfasst
|
memory = _take_retrieve(self.perf0) # Gedächtnis-Retrieve (HTTP-Rückruf), best-effort
|
||||||
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
# Generierung = alles nach dem ersten Inhalts-Token bis Stream-Ende.
|
||||||
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
gen = round(total - self.hirn_ms, 1) if self.hirn_ms is not None else None
|
||||||
rec = {
|
rec = {
|
||||||
@@ -151,7 +152,7 @@ class TurnTrace:
|
|||||||
"vision_ms": self.vision_ms,
|
"vision_ms": self.vision_ms,
|
||||||
"hirn_ms": self.hirn_ms,
|
"hirn_ms": self.hirn_ms,
|
||||||
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
"gen_ms": gen if (gen is None or gen >= 0) else 0.0,
|
||||||
"mem0_ms": mem0,
|
"memory_ms": memory,
|
||||||
"total_ms": round(total, 1),
|
"total_ms": round(total, 1),
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-9
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
MC2-Steward — die Wächter-Loops als EIGENER Prozess (UMBAU v3, P2).
|
||||||
|
|
||||||
Bisher hingen Re-Warm-Wächter, Health-Wächter (sentry) und Mem0-Auto-Dedupe am
|
Bisher hingen Re-Warm-Wächter und Health-Wächter (sentry) am
|
||||||
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
Lebenszyklus des Steuerpult-Webservers (app.py-Lifespan): jeder MC2-Neustart riss
|
||||||
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
den Wächtern Timing und Flanken-Gedächtnis weg — und ein TOTES Steuerpult konnte
|
||||||
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
sich prinzipbedingt nicht selbst melden. Hier laufen DIESELBEN Loops (unveränderte
|
||||||
@@ -10,15 +10,15 @@ Module) als eigener Mini-Dienst (mc2-steward.service, Restart=always):
|
|||||||
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
• warmer.rewarm_loop — Warm-Set nachladen (+ Config-Watch ersetzt den
|
||||||
In-Process-Nudge aus llamaswap.write_config)
|
In-Process-Nudge aus llamaswap.write_config)
|
||||||
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
• sentry.sentry_loop — Health-Flanken → Briefkasten (HTTP an MC2) + Telegram;
|
||||||
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
beobachtet im Steward-Modus AUCH MC2 selbst + mc2-gateway
|
||||||
• memory.auto_dedupe_loop — Gedächtnis-Dubletten (HTTP an den Mem0-Sidecar)
|
|
||||||
|
|
||||||
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
BEWUSST NICHT hier: reminders_loop — der teilt sich Datei UND CRUD-Pfade mit dem
|
||||||
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
/api/reminders-Router (Zwei-Schreiber-Risiko auf mc2-reminders.json); er bleibt im
|
||||||
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
Steuerpult. Der Briefkasten-Store gehört weiter EXKLUSIV dem MC2-Prozess — dieser
|
||||||
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
Prozess liefert Meldungen per HTTP ab (announce.py, MC_ANNOUNCE_HTTP).
|
||||||
|
|
||||||
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED / MC_MEM_DEDUPE_ENABLED)
|
Die Loop-Schalter (MC_REWARM_ENABLED / MC_SENTRY_ENABLED)
|
||||||
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
stehen in der MC2-Unit auf 0 und hier auf Default 1 — reiner Konfig-Split, kein
|
||||||
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
Verhaltens-Code im Steuerpult angefasst. Zeilen dort entfernen = Rollback.
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +28,6 @@ import logging
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from config import CONFIG_PATH
|
from config import CONFIG_PATH
|
||||||
from services import memory as memory_svc
|
|
||||||
from services import sentry, warmer
|
from services import sentry, warmer
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -66,10 +65,6 @@ async def main() -> None:
|
|||||||
warmer.INTERVAL, CONFIG_WATCH_S)
|
warmer.INTERVAL, CONFIG_WATCH_S)
|
||||||
if sentry.ENABLED:
|
if sentry.ENABLED:
|
||||||
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
tasks.append(asyncio.create_task(sentry.sentry_loop()))
|
||||||
if memory_svc.AUTO_DEDUPE_ENABLED:
|
|
||||||
tasks.append(asyncio.create_task(memory_svc.auto_dedupe_loop()))
|
|
||||||
log.info("Mem0-Auto-Dedupe aktiv (alle %ss, Schwelle %s)",
|
|
||||||
memory_svc.AUTO_DEDUPE_INTERVAL, memory_svc.AUTO_DEDUPE_THRESHOLD)
|
|
||||||
if not tasks:
|
if not tasks:
|
||||||
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
log.warning("steward: alle Loops per Env deaktiviert — nichts zu tun, Ende.")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ hint=""
|
|||||||
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
# Stabile Anti-Flail-Orientierung: WER bin ich, WO laufen die Modelle, WAS ist tabu.
|
||||||
ctx="$(cat <<EOF
|
ctx="$(cat <<EOF
|
||||||
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
[BOX-ORIENTIERUNG — du bist ein Hintergrund-Worker auf der lokalen AI-Box (Linux, Zeit UTC / lokal Europe/Berlin, locale de_DE), NICHT Lucy und kein Dev-Laptop. Du arbeitest EINE Kanban-Aufgabe ab.]
|
||||||
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642, mem0-Gedaechtnis :8765. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
Die Modelle / das "Hirn" laufen ausschliesslich ueber den Router llama-swap auf 127.0.0.1:8080 (startet je Modell einen llama-server) bzw. das OpenAI-kompatible Gateway von MC2 auf 127.0.0.1:9001/v1 — es gibt KEINEN anderen Modell-Server, also nicht danach suchen. Weitere Dienste: hermes-gateway :8642. Der MC2-Live-Checkout ~/mission-control-v2 ist nur zum LESEN da (Schreiben/committen TABU — Code-Arbeit immer im frischen Klon deines Task-Workspaces). Grenzen (was gehoert wohin): MC2 = Steuerpult (verwalten/klicken/zusehen, KEIN Chat-UI), Lucy = Chat & Stimme (eigenes Repo), Hermes-Quelle ist TABU (Agenten-Verhalten nur ueber Config/SOUL/Skill/Hook/MCP aendern). Feste Entscheide/Verbote + volle Grenzen-Landkarte: ~/mission-control-v2/docs/wissen/ (GRENZEN/VERDIKTE/FALLEN/STACK) + ~/wissens-vault/eigenbau-landkarte.md.${hint}
|
||||||
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
TRIAGE (19.07. — Drei-Bahnen-Regel): Ordne deine Karte ZUERST einer Bahn zu und bleib darin: Denken (Konzept/Plan), Bauen (Code schreiben/aendern), Betrieb (SSH/Deploy/Dienste). Fuer Betrieb-Arbeit lade IMMER zuerst den Skill betrieb-playbook (Diagnose-Checklisten; max 3 Anlaeufe pro Hindernis, dann kanban_block mit exaktem Fehlertext — die No-Progress-Bremse erzwingt das mechanisch).
|
||||||
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
PROTOKOLL-PFLICHT (15.07.): Beende deine Aufgabe IMMER mit dem Tool-Aufruf kanban_complete (fertig) oder kanban_block (Frage/Sackgasse). Eine Text-Zusammenfassung OHNE diesen Aufruf zaehlt als Absturz (Protokollverstoss) und blockiert die Karte — genau so ging am 14.07. eine fertige Arbeit verloren.
|
||||||
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
REPO-WEGWEISER mission-control-v2 (NICHT raten/suchen — so liegt es): backend/app.py = Einstieg + Router-Mounting · backend/routers/*.py = REST-Endpunkte · backend/services/*.py = Logik · backend/gateway_app.py = /v1-Gateway-Prozess (:9010) · frontend/src/views/*.tsx = Ansichten · frontend/src/lib/api.ts = Typen+Fetch · frontend/src/lib/queries.ts = TanStack-Query-Hooks (Polling) · frontend/src/nav.ts = Sidebar · deploy/ = Skripte/Units/Hooks/Skills · Projekt-Regeln: AGENTS.md (Wurzel) + docs/wissen/.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ TOKEN=$(grep -m1 'git\.tobisniceshomelab' "$HOME/.git-credentials" 2>/dev/null |
|
|||||||
[ -n "$TOKEN" ] || exit 0
|
[ -n "$TOKEN" ] || exit 0
|
||||||
|
|
||||||
python3 - "$TOKEN" "$STATE" <<'PY'
|
python3 - "$TOKEN" "$STATE" <<'PY'
|
||||||
import json, sys, urllib.request
|
import json, sys, urllib.request, subprocess, os
|
||||||
|
|
||||||
token, state_pfad = sys.argv[1], sys.argv[2]
|
token, state_pfad = sys.argv[1], sys.argv[2]
|
||||||
# API intern (die DDNS-Domain ist nachts wegen Zwangstrennung oft tot, 24.07.2026);
|
# API intern (die DDNS-Domain ist nachts wegen Zwangstrennung oft tot, 24.07.2026);
|
||||||
@@ -79,6 +79,14 @@ if neu:
|
|||||||
for full, sha, name in neu:
|
for full, sha, name in neu:
|
||||||
print(f"• {full} @ {sha} ({name})")
|
print(f"• {full} @ {sha} ({name})")
|
||||||
print(f" {WEB}/{full}/actions")
|
print(f" {WEB}/{full}/actions")
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
os.path.expanduser("~/.local/bin/hermes"), "kanban", "create",
|
||||||
|
"--title", f"CI rot: {full} @ {sha}",
|
||||||
|
"--body", f"Der CI Lauf '{name}' in {full} ist fehlgeschlagen. Bitte Log ansehen und Fix vorschlagen."
|
||||||
|
], capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler bei kanban_create: {e}", file=sys.stderr)
|
||||||
print("Nichts aus diesen Ständen ist ‚fertig', solange die Ampel rot ist.")
|
print("Nichts aus diesen Ständen ist ‚fertig', solange die Ampel rot ist.")
|
||||||
|
|
||||||
with open(state_pfad, "w") as f:
|
with open(state_pfad, "w") as f:
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ check_hermes(){
|
|||||||
done
|
done
|
||||||
if [ "$state" = "done" ]; then
|
if [ "$state" = "done" ]; then
|
||||||
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
SUMMARY+=("$name: $behind Commits eingespielt, Gehirn-Check grün.")
|
||||||
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Mem0, Tools, Voice) grün — alles läuft."
|
notify "Hermes-Agent aktualisiert ($behind Commits). Gehirn-Check (Gedächtnis, Tools, Voice) grün — alles läuft."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
# Rot/Timeout: ERST Selbstreparatur versuchen (Config-Bruch selbst ziehen, neue Version behalten) —
|
||||||
|
|||||||
+6
-9
@@ -3,8 +3,8 @@
|
|||||||
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
# zurückkommt. Erzeugt EIN Tarball mc2-state-<ts>.tar.gz, behält die letzten N.
|
||||||
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
# Läuft per systemd-Timer (täglich), manuell, oder über den UI-Snapshot-Button.
|
||||||
#
|
#
|
||||||
# Inhalt: mem0 (Chroma + history.db) · ~/.hermes (config.yaml, .env, plugins/) ·
|
# Inhalt: ~/.hermes (config.yaml, .env, plugins/) · /etc/llama-swap/config.yaml ·
|
||||||
# /etc/llama-swap/config.yaml · known-good/ (Versions-Manifest, s. u.)
|
# known-good/ (Versions-Manifest, s. u.)
|
||||||
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
# NICHT enthalten (bewusst): GGUF-Modelle (riesig, neu ladbar), MC2-Code (Git), venvs.
|
||||||
#
|
#
|
||||||
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
# ACHTUNG: das Tarball enthält ~/.hermes/.env (Secrets) → chmod 600, nicht in Git.
|
||||||
@@ -20,16 +20,14 @@ RETAIN="${MC_BACKUP_RETAIN:-14}"
|
|||||||
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
# Off-Box-Ziel (leer = deaktiviert). Default: Proxmox-Host, eigener Key mit minimalem Recht.
|
||||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
TS="$(date +%Y%m%d-%H%M%S)"
|
TS="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
|
||||||
STAGE="$(mktemp -d)"
|
STAGE="$(mktemp -d)"
|
||||||
trap 'rm -rf "$STAGE"' EXIT
|
trap 'rm -rf "$STAGE"' EXIT
|
||||||
mkdir -p "$STAGE/mem0" "$STAGE/hermes" "$STAGE/llama-swap"
|
mkdir -p "$STAGE/hermes" "$STAGE/llama-swap"
|
||||||
|
|
||||||
[ -d "$MEM0_DIR" ] && cp -a "$MEM0_DIR/." "$STAGE/mem0/" || true
|
|
||||||
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
[ -f "$HERMES/config.yaml" ] && cp -a "$HERMES/config.yaml" "$STAGE/hermes/" || true
|
||||||
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
[ -f "$HERMES/.env" ] && cp -a "$HERMES/.env" "$STAGE/hermes/" || true
|
||||||
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
[ -d "$HERMES/plugins" ] && cp -a "$HERMES/plugins" "$STAGE/hermes/plugins" || true
|
||||||
@@ -60,7 +58,7 @@ SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
|||||||
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
HERMES_AGENT="${HERMES_AGENT_DIR:-$HOME/.hermes/hermes-agent}"
|
||||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||||
|
|
||||||
# pip freeze je venv; mem0-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
# pip freeze je venv; voice-venv ist uv-managed (kein pip-Modul) → uv als Fallback.
|
||||||
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
||||||
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
if [ ! -x "$1" ]; then echo "venv fehlt: $1" > "$2"; return 0; fi
|
||||||
"$1" -m pip freeze > "$2" 2>/dev/null \
|
"$1" -m pip freeze > "$2" 2>/dev/null \
|
||||||
@@ -68,7 +66,6 @@ freeze_venv() { # $1 = python-Pfad, $2 = Zieldatei
|
|||||||
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
|| echo "pip freeze fehlgeschlagen: $1" > "$2"
|
||||||
}
|
}
|
||||||
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
freeze_venv "$SRC/backend/.venv/bin/python" "$KG/pip-backend.txt"
|
||||||
freeze_venv "$HOME/.mem0/venv/bin/python" "$KG/pip-mem0.txt"
|
|
||||||
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -79,7 +76,7 @@ freeze_venv "$HOME/.voice/venv/bin/python" "$KG/pip-voice.txt"
|
|||||||
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
echo "llama.cpp : $(LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -m1 -o 'version: .*' || echo '?')"
|
||||||
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
echo "llama-swap : $(/usr/local/bin/llama-swap --version 2>/dev/null | head -1 || echo '?')"
|
||||||
echo "python-venvs :"
|
echo "python-venvs :"
|
||||||
for P in "$SRC/backend/.venv/bin/python" "$HOME/.mem0/venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
for P in "$SRC/backend/.venv/bin/python" "$HOME/.voice/venv/bin/python"; do
|
||||||
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
echo " $P → $("$P" --version 2>&1 || echo 'fehlt')"
|
||||||
done
|
done
|
||||||
} > "$KG/versions.txt" || true
|
} > "$KG/versions.txt" || true
|
||||||
@@ -92,7 +89,7 @@ cat > "$STAGE/MANIFEST.txt" <<EOF
|
|||||||
mc2-state backup
|
mc2-state backup
|
||||||
created : $TS
|
created : $TS
|
||||||
host : $(hostname)
|
host : $(hostname)
|
||||||
inhalt : mem0 (chroma + history.db), hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
inhalt : hermes (config.yaml, .env, plugins/), llama-swap (config.yaml), known-good/ (Versions-Manifest: pip freeze, Engine, Modelle)
|
||||||
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
restore : bash ~/mission-control-v2/deploy/restore.sh mc2-state-$TS.tar.gz
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
+23
-2
@@ -6,14 +6,35 @@ echo "Starte MC2 Deployment..."
|
|||||||
# 1. Neuesten Code holen
|
# 1. Neuesten Code holen
|
||||||
git pull origin main
|
git pull origin main
|
||||||
|
|
||||||
|
|
||||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||||
echo "Aktualisiere Python Abhängigkeiten..."
|
echo "Aktualisiere Python Abhängigkeiten..."
|
||||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||||
|
|
||||||
# 3. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
# 3. Llama-Swap Konfiguration prüfen und synchronisieren
|
||||||
|
if ! cmp -s deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml; then
|
||||||
|
echo "Llama-Swap Konfiguration hat sich geändert, synchronisiere..."
|
||||||
|
# NOPASSWD für llama-swap Neustart ist vorausgesetzt (siehe AGENTS.md)
|
||||||
|
sudo cp deploy/llama-swap.config.yaml /etc/llama-swap/config.yaml
|
||||||
|
sudo systemctl restart llama-swap
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
|
|
||||||
# 4. Dienste neu starten
|
# 4.5 Hermes Skills synchronisieren
|
||||||
|
echo "Synchronisiere Hermes Skills..."
|
||||||
|
mkdir -p ~/.hermes/skills
|
||||||
|
# Kopiere alle Skills aus dem Repo, ersetze - durch _ in den Ordnernamen für Hermes Kompatibilität
|
||||||
|
for skill_dir in deploy/skills/*; do
|
||||||
|
if [ -d "$skill_dir" ]; then
|
||||||
|
skill_name=$(basename "$skill_dir" | tr '-' '_')
|
||||||
|
mkdir -p ~/.hermes/skills/"$skill_name"
|
||||||
|
cp -r "$skill_dir"/* ~/.hermes/skills/"$skill_name"/
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 5. Dienste neu starten
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Alte Crons löschen
|
|
||||||
hermes cron remove traum || true
|
|
||||||
hermes cron remove chef-gutachter || true
|
|
||||||
hermes cron remove release-radar || true
|
|
||||||
hermes cron remove idle-radar || true
|
|
||||||
hermes cron remove selbst-inventur || true
|
|
||||||
hermes cron remove karten-gutachter || true
|
|
||||||
hermes cron remove pruefstand || true
|
|
||||||
hermes cron remove trend-radar || true
|
|
||||||
|
|
||||||
# Neue Crons registrieren mit Wrapper und --deliver origin.
|
|
||||||
# Dadurch schweigt Hermes cron selbst, und der Wrapper nutzt notify.sh für den Morgen-Digest.
|
|
||||||
hermes cron create --name traum --deliver origin "15 3 * * *" "Traum dreaming-feed.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name chef-gutachter --deliver origin "30 4 * * *" "Chef-Gutachter chef-gutachter-feed.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name release-radar --deliver origin "15 5 * * 1" "Release-Radar hermes-release-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name idle-radar --deliver origin "45 3 * * *" "Idle-Radar idle-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name selbst-inventur --deliver origin "35 3 * * *" "Selbst-Inventur selbst-inventur.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name karten-gutachter --deliver origin "0 4 * * *" "Karten-Gutachter karten-gutachter.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name pruefstand --deliver origin "0 2 * * *" "Prüfstand pruefstand.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
hermes cron create --name trend-radar --deliver origin "15 5 * * 6" "Trend-Radar trend-radar-feed.sh" --script night-cron-wrapper.sh --no-agent
|
|
||||||
|
|
||||||
echo "Crons erfolgreich gefixt!"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Arbeitsregeln (Aider liest diese Datei als schreibgeschützten Kontext)
|
|
||||||
|
|
||||||
Diese Regeln gelten für JEDE Sitzung. Sie sind der Kern der Session-Hygiene:
|
|
||||||
Wissen lebt in `SAVEPOINT.md` und in der git-Historie, NICHT im Chat-Verlauf.
|
|
||||||
|
|
||||||
## 1. Zu Beginn: erst SAVEPOINT.md lesen
|
|
||||||
Bevor du irgendetwas tust, lies `SAVEPOINT.md` vollständig. Es ist die Wahrheit über
|
|
||||||
den aktuellen Stand. Richte dich danach — nicht nach Annahmen. Erfinde keinen Kontext,
|
|
||||||
der nicht in `SAVEPOINT.md`, im Code oder in der git-Historie steht. Wenn etwas unklar
|
|
||||||
ist, sag es, statt es zu erfinden.
|
|
||||||
|
|
||||||
## 2. Nach jedem sinnvollen Schritt: SAVEPOINT.md aktualisieren
|
|
||||||
Sobald du eine sinnvolle Änderung abgeschlossen hast (eine Funktion, ein Fix, ein
|
|
||||||
Testlauf), aktualisiere `SAVEPOINT.md`. Halte es kurz und ehrlich. Struktur:
|
|
||||||
|
|
||||||
- **Ziel** — was insgesamt gebaut werden soll (ein bis zwei Sätze).
|
|
||||||
- **Erledigt** — was jetzt wirklich funktioniert (nur Bewiesenes; keine Fassade).
|
|
||||||
- **Nächster Schritt** — die genau eine Sache, die als Nächstes zu tun ist.
|
|
||||||
- **Offene Fragen** — Entscheidungen, die noch anstehen.
|
|
||||||
- **Stolpersteine** — alles, worüber eine frische Sitzung sonst stolpern würde.
|
|
||||||
- **Dateien** — die wichtigsten Dateien und was sie enthalten.
|
|
||||||
|
|
||||||
Schreibe es so, dass eine frische Sitzung OHNE jede Erinnerung allein aus `SAVEPOINT.md`
|
|
||||||
plus git sauber weitermachen kann. Das ist der Test: kein verstecktes Wissen im Chat.
|
|
||||||
|
|
||||||
## 3. Kleine, überprüfbare Schritte
|
|
||||||
Ändere wenig pro Runde. Behaupte nichts als fertig, was du nicht geprüft hast. Wenn ein
|
|
||||||
Test existiert, nenne sein Ergebnis. Wenn du unsicher bist, prüfe, statt zu raten.
|
|
||||||
|
|
||||||
## 4. Wenn der Governor das Sitzungs-Limit meldet
|
|
||||||
Erscheint eine Nachricht mit `[GOVERNOR — SITZUNGS-LIMIT ERREICHT]`, dann beginne KEINE
|
|
||||||
neuen Code-Änderungen mehr. Finalisiere nur `SAVEPOINT.md` (Stand vollständig, nächster
|
|
||||||
Schritt präzise) und weise den Nutzer an, eine frische Sitzung zu starten. Sonst nichts.
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
# Governor — Phase 0
|
|
||||||
|
|
||||||
Dünner, zustandsloser Token-Wächter-Proxy zwischen einem Off-the-shelf-Coding-Agenten
|
|
||||||
(**Aider**) und dem lokalen Modell-Endpoint (llama-swap `:8080`). Er erzwingt
|
|
||||||
**Session-Hygiene per hartem Schnitt statt Auto-Compaction**: wenn die Anfrage (= ganze
|
|
||||||
Sitzungshistorie, die jede Runde mitkommt) eine Schwelle übersteigt, schiebt er eine
|
|
||||||
Anweisung ein, `SAVEPOINT.md` zu finalisieren und zu stoppen — damit Wissen in
|
|
||||||
`SAVEPOINT.md` + git lebt, nicht im degradierenden Chat-Kontext.
|
|
||||||
|
|
||||||
Das ist **Phase 0** des Ablöse-Plans „Lucy IDE-Modus + Governor": den Kern beweisen,
|
|
||||||
**ohne** eine Zeile Lucy-Code. Kein bespoke Editor, kein Aider-Fork, kein Modelltausch.
|
|
||||||
|
|
||||||
## Bausteine (dieses Verzeichnis)
|
|
||||||
| Datei | Zweck |
|
|
||||||
|---|---|
|
|
||||||
| `governor.py` | Der Proxy. Nur Standardbibliothek (läuft mit System-`python3`), zustandslos. |
|
|
||||||
| `CONVENTIONS.md` | Aiders schreibgeschützte Arbeitsregeln: SAVEPOINT.md zuerst lesen, laufend pflegen, keine Fassade. |
|
|
||||||
| `SAVEPOINT.template.md` | Anfangs-Savepoint für ein frisches Projekt. |
|
|
||||||
| `driver.py` | Treibt eine akkumulierende Aider-Sitzung über die Scripting-API (eine Zeile = eine Runde). |
|
|
||||||
| `gov-ctl.sh` | Governor sauber starten/stoppen/status (detached via `setsid`). |
|
|
||||||
| `run-driver.sh` | `run-driver.sh <msgs> [repo]` — Aider-Sitzung durch den Governor. |
|
|
||||||
| `reset-repo.sh` | Wegwerf-Test-Repo frisch aufsetzen. |
|
|
||||||
| `hardstop-test.sh` | Direkte curls für die drei Pfade (passthrough / soft / hart). |
|
|
||||||
| `msgs-*.txt` | Nachrichtenskripte für die Testläufe. |
|
|
||||||
|
|
||||||
## Verhalten des Governors
|
|
||||||
Pro `/v1/chat/completions`-Anfrage schätzt er die Tokenzahl (`Zeichen / GOV_CHARS_PER_TOKEN`,
|
|
||||||
kalibriert auf CPT **3.5** → `est ≈ echte prompt_tokens` auf ~1–3 % bei echten Sessions):
|
|
||||||
|
|
||||||
- **est < Soft** → unverändert durchreichen.
|
|
||||||
- **est ≥ Soft (`GOV_THRESHOLD`, Default 25000)** → hängt die SAVEPOINT-Stopp-Anweisung als
|
|
||||||
letzte User-Nachricht an, leitet weiter, loggt `FIRED`. Das Modell schreibt EINEN
|
|
||||||
ehrlichen Abschluss-Savepoint.
|
|
||||||
- **est ≥ Hart (`GOV_HARD_CEILING`, Default 0 = aus)** → der Governor antwortet SELBST mit
|
|
||||||
einer kurzen Stopp-Nachricht, **ohne** das Modell zu fragen; loggt `HARDSTOP`. Verhindert,
|
|
||||||
dass über die Grenze hinaus weitergearbeitet wird (siehe Befund unten).
|
|
||||||
|
|
||||||
Alle anderen Pfade (`/v1/models` etc.) werden roh durchgereicht. Streaming (SSE) wird
|
|
||||||
byteweise durchgereicht; die echten `prompt_tokens` aus der Antwort werden zur Kalibrierung
|
|
||||||
mitgeloggt.
|
|
||||||
|
|
||||||
### Sprach-Signal an Lucy (Phase 2)
|
|
||||||
Beim Feuern (soft ODER hart) POSTet der Governor — best-effort, gedrosselt (Default 300 s,
|
|
||||||
damit die Pro-Runde-Feuerung nicht spammt) — eine Meldung an Lucys vorhandene Announce-Pipeline
|
|
||||||
(`POST :9001/api/voice/announce`, `source:governor`, `priority:normal`). Lucy pollt diese Queue
|
|
||||||
ohnehin, dedupliziert per Cursor und spricht sie über ihr lokales TTS — gated durch ihren
|
|
||||||
„Box-Meldungen laut"-Schalter. **Kein Lucy-Code nötig.** Abschalten: `GOV_ANNOUNCE_URL=""`.
|
|
||||||
|
|
||||||
### Umgebungsvariablen
|
|
||||||
`GOV_PORT` (8100) · `GOV_HOST` (0.0.0.0) · `GOV_UPSTREAM` (http://127.0.0.1:8080) ·
|
|
||||||
`GOV_THRESHOLD` (25000) · `GOV_HARD_CEILING` (Default Soft+5000; 0=aus) · `GOV_CHARS_PER_TOKEN` (3.5) ·
|
|
||||||
`GOV_LOG` · `GOV_DIRECTIVE` · `GOV_HARDSTOP_MSG` · `GOV_ANNOUNCE_URL` (:9001/api/voice/announce; ""=aus) ·
|
|
||||||
`GOV_ANNOUNCE_THROTTLE` (300 s) · `GOV_ANNOUNCE_TEXT`.
|
|
||||||
|
|
||||||
## Auf der Box laufen lassen (wie in P0 aufgesetzt)
|
|
||||||
```bash
|
|
||||||
# Aider (einmalig, unter isoliertem Python 3.12 — System-Python 3.14 bricht Aiders Pins):
|
|
||||||
pipx install uv && uv tool install --python 3.12 aider-chat
|
|
||||||
|
|
||||||
# Dateien liegen in ~/governor-p0/. Governor starten (Hart-Deckel default AN = Soft+5000):
|
|
||||||
~/governor-p0/gov-ctl.sh start 25000 # Soft 25k, Hart 30k (auto)
|
|
||||||
# ~/governor-p0/gov-ctl.sh start 25000 0 # Hart AUS (nur weicher Schnitt)
|
|
||||||
|
|
||||||
# Aider-Sitzung durch den Governor:
|
|
||||||
~/governor-p0/run-driver.sh ~/governor-p0/msgs-todo.txt
|
|
||||||
```
|
|
||||||
Aider zeigt mit `OPENAI_API_BASE=http://127.0.0.1:8100/v1` und Modell
|
|
||||||
`openai/Qwen3-Coder-Next` auf den Governor.
|
|
||||||
|
|
||||||
## Wichtig: Aiders eigene Zusammenfassung MUSS aus
|
|
||||||
Der Treiber setzt `coder.summarizer.max_tokens` auf ~1e9. Sonst fasst Aider die Historie
|
|
||||||
selbst zusammen (Auto-Compaction) und die Anfrage wächst nie bis zur Schwelle — der
|
|
||||||
Governor wäre ausgehebelt, und man bekäme genau die über-komprimierte Halluzination, die
|
|
||||||
der Plan verwirft. Der Governor soll die **alleinige** Sitzungsgrenze sein.
|
|
||||||
|
|
||||||
## Ergebnisse & Befunde (24.07.2026)
|
|
||||||
|
|
||||||
### Akzeptanz — alle vier Kriterien bewiesen (Box, Qwen3-Coder-Next)
|
|
||||||
1. **Governor zählt + feuert an der Schwelle** — 12-Runden-Todo-Lauf (Soft 8000): Runden 1-6
|
|
||||||
`ok`, ab Runde 7 `FIRED` (est 8546 / echt 8652). Hart-Deckel: Anfrage mit est 11429 ≥ 10000
|
|
||||||
→ `HARDSTOP`, Governor antwortet selbst (kein Modell-Call). Alles im Log.
|
|
||||||
2. **Aider pflegt SAVEPOINT.md** — über den ganzen Aufbau hinweg strukturiert gehalten
|
|
||||||
(Ziel/Erledigt/Nächster Schritt/Stolpersteine/Dateien) gemäß `CONVENTIONS.md`.
|
|
||||||
3. **An der Grenze: ehrlicher Abschluss + Stopp** — beim ersten Feuern (Runde 7) schrieb das
|
|
||||||
Modell einen **ehrlichen** Savepoint (nur real Gebautes unter „Erledigt", Tests als nächster
|
|
||||||
Schritt) und verweigerte neuen Code.
|
|
||||||
4. **Frische Sitzung macht sauber weiter — keine Fassade** — neue Aider-Sitzung las den
|
|
||||||
Grenz-Savepoint, baute `test_todo.py` (der exakte nächste Schritt), und **alle 9 unittest-
|
|
||||||
Tests laufen grün** gegen die echte API. Kein Erfinden.
|
|
||||||
|
|
||||||
### Kalibrierung
|
|
||||||
`CHARS_PER_TOKEN = 3.5` → `est` traf die echten `prompt_tokens` bei realen Sessions auf ~1-3 %.
|
|
||||||
(Nur künstlicher, extrem repetitiver Fülltext bricht die Heuristik — irrelevant für echten Code.)
|
|
||||||
Der Coder läuft mit 128k Kontext (`-c 131072`), also keine Modell-Kappung bei 25k.
|
|
||||||
|
|
||||||
### Wichtigster Befund: Soft reicht nicht allein → Hart-Deckel nachgerüstet
|
|
||||||
Der **weiche** Schnitt erzeugt genau EINEN ehrlichen Grenz-Savepoint — solange die Grenze
|
|
||||||
respektiert wird. Schickt man aber über die Grenze hinaus weiter Aufträge (wie im Stresstest),
|
|
||||||
verweigert das Modell zwar den Code, schreibt die Absichten aber fortschreitend als „erledigt"
|
|
||||||
in SAVEPOINT — genährt von Aiders **irreführenden Commit-Nachrichten** (die aus dem SAVEPOINT-
|
|
||||||
Absichtstext geschöpft werden). Ergebnis: eine Fassade (behauptete test_todo.py/README, die es
|
|
||||||
nicht gab). Deshalb der optionale **Hart-Deckel** (`GOV_HARD_CEILING`): oberhalb davon antwortet
|
|
||||||
der Governor selbst, das Modell kann keine degradierenden Savepoints mehr schreiben. **Der Hart-
|
|
||||||
Deckel ist jetzt Default AN** (`GOV_HARD_CEILING` unset → Soft+5000; explizit `0` schaltet ihn
|
|
||||||
aus): ein Finalisier-Zug Luft, dann harter Riegel — das schliesst die Fassaden-Lücke.
|
|
||||||
|
|
||||||
### Weitere Befunde / Fallen
|
|
||||||
- **Aiders eigene Zusammenfassung MUSS aus** (`summarizer.max_tokens` hoch) — sonst compactet
|
|
||||||
Aider selbst und der Governor greift nie. Siehe oben.
|
|
||||||
- **Commit-Nachrichten überzeichnen** in der Abschluss-Phase (aus SAVEPOINT-Absicht). Der Code
|
|
||||||
ist die Wahrheit; git-Nachrichten sind es hier nicht. Der Hart-Deckel (jetzt Default) begrenzt
|
|
||||||
das auf ~1 Zug; wer es ganz sauber will, startet Aider mit `--no-auto-commits` (Commits von Hand).
|
|
||||||
- **Python 3.14 auf der Box bricht Aiders Pins** (numpy 1.24.3) → Aider via `uv` unter isoliertem
|
|
||||||
Python 3.12 installiert.
|
|
||||||
- **Aider-Scripting-API (`coder.run`) hängt** in einer Datei-Hinzufügen-Reflexion (Edits landeten
|
|
||||||
nicht). Für Einzel-Runden `aider --message` nutzen (sauberer, unterstützt). Der `driver.py`
|
|
||||||
taugt für Mehr-Runden-Akkumulation (Governor-Test), nicht als Produktions-Treiber.
|
|
||||||
|
|
||||||
### Bekannte Grenzen des Governors (aus adversarialer Review, für später)
|
|
||||||
- **Chunked Request-Bodies ohne `Content-Length`** werden verworfen (Aiders httpx sendet immer
|
|
||||||
`Content-Length` → schlummernd, aber ein Proxy-Hop mit Chunking bräche).
|
|
||||||
- **Tool-/Function-Calling**: der Soft-Einschub als letzte `user`-Nachricht kann die Nachrichten-
|
|
||||||
reihenfolge stören, wenn Aider ein Tool-Calling-Edit-Format nutzt (Aiders diff/whole sind reiner
|
|
||||||
Text → schlummernd). `estimate_tokens` zählt `tools`/`tool_calls` nicht mit.
|
|
||||||
- **`https://`-Upstream** wird nicht unterstützt (nur `http.client.HTTPConnection`). Für den
|
|
||||||
lokalen `:8080`-Endpoint irrelevant.
|
|
||||||
Behoben aus derselben Review: **inkrementelles Streaming** (`read1()` statt `read()` — vorher
|
|
||||||
puffernd), **Config-Crash** bei `{ }` in `GOV_DIRECTIVE` (sichere Substitution), **Query-String**
|
|
||||||
umging die Erkennung, **Socket-Leak** im Fehlerpfad, doppelte `Date`/`Server`-Header.
|
|
||||||
|
|
||||||
## Nächste Schritte (Phase 1+)
|
|
||||||
Terminal in Lucy einbetten (xterm.js + node-pty, MC2-Muster kopieren) → Voice-Hook auf das
|
|
||||||
Governor-Signal → Feinschliff + Aider/Pi-Finalentscheid. Governor evtl. später in mc2-gateway.
|
|
||||||
Kandidat für Phase 1-Härtung: Auto-Commit-Zügelung + Hart-Deckel als Default.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# SAVEPOINT
|
|
||||||
|
|
||||||
> Lebende Übergabe-Datei. Die aktuelle Sitzung hält sie fortlaufend aktuell; eine
|
|
||||||
> frische Sitzung liest sie ZUERST und macht allein daraus plus git weiter.
|
|
||||||
> (Anfangszustand — von der ersten Sitzung zu ersetzen.)
|
|
||||||
|
|
||||||
## Ziel
|
|
||||||
_(noch nichts — von der ersten Sitzung zu füllen)_
|
|
||||||
|
|
||||||
## Erledigt
|
|
||||||
- _(noch nichts)_
|
|
||||||
|
|
||||||
## Nächster Schritt
|
|
||||||
- Auftrag des Nutzers entgegennehmen und beginnen.
|
|
||||||
|
|
||||||
## Offene Fragen
|
|
||||||
- _(keine)_
|
|
||||||
|
|
||||||
## Stolpersteine
|
|
||||||
- _(keine bekannt)_
|
|
||||||
|
|
||||||
## Dateien
|
|
||||||
- `SAVEPOINT.md` — diese Übergabe-Datei.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# announce-test.sh — loest ein Feuern aus und prueft, ob der Governor das Sprach-
|
|
||||||
# Signal an Lucys Announce-Queue (:9001) postet (Phase 2). Voraussetzung: Governor
|
|
||||||
# mit niedriger Schwelle gestartet, damit ein einzelner Request feuert.
|
|
||||||
set -u
|
|
||||||
GOV="http://127.0.0.1:8100/v1/chat/completions"
|
|
||||||
|
|
||||||
python3 - <<'PY' > /tmp/gov_fire.json
|
|
||||||
import json
|
|
||||||
print(json.dumps({"model":"Qwen3-Coder-Next","messages":[{"role":"user","content":"y"*35000}],"max_tokens":8,"stream":False}))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "=== Feuern ausloesen (est ~10000 Tokens, ueber Hart-Deckel) ==="
|
|
||||||
curl -s -m 20 "$GOV" -H "Content-Type: application/json" --data @/tmp/gov_fire.json | jq '{id, finish: .choices[0].finish_reason}'
|
|
||||||
|
|
||||||
sleep 1.5 # Announce-Thread durchlassen
|
|
||||||
echo "=== Governor-Log (letzte 4 Zeilen) ==="
|
|
||||||
tail -4 ~/governor-p0/governor.log
|
|
||||||
|
|
||||||
echo "=== Announce-Queue: Governor-Eintraege ==="
|
|
||||||
curl -s -m 8 "http://127.0.0.1:9001/api/voice/announcements?after=0&limit=100" \
|
|
||||||
| jq '[.items[] | select(.source=="governor")] | (last // "KEINE governor-Meldung gefunden")'
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""driver.py — treibt EINE Aider-Sitzung ueber die Scripting-API durch den Governor.
|
|
||||||
|
|
||||||
Jede Zeile der Nachrichtendatei ist eine User-Runde. Weil derselbe Coder alle Runden
|
|
||||||
bedient, akkumuliert die Historie und die Anfrage waechst jede Runde — genau das, was
|
|
||||||
der Governor beobachtet. Aiders EIGENE History-Zusammenfassung wird abgeschaltet, damit
|
|
||||||
der Governor die alleinige Sitzungsgrenze ist (der Plan verwirft Auto-Compaction bewusst).
|
|
||||||
|
|
||||||
Aufruf (cwd muss das Test-Repo sein, mit venv-python):
|
|
||||||
.../aider-chat/bin/python driver.py <messages-file>
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from aider.coders import Coder
|
|
||||||
from aider.io import InputOutput
|
|
||||||
from aider.models import Model
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
# Endpoint VOR den Aider-Aufrufen setzen (litellm liest es zur Laufzeit).
|
|
||||||
os.environ.setdefault("OPENAI_API_BASE", "http://127.0.0.1:8100/v1")
|
|
||||||
os.environ.setdefault("OPENAI_API_KEY", "dummy")
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("usage: driver.py <messages-file>", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
||||||
messages = [ln.strip() for ln in fh if ln.strip() and not ln.lstrip().startswith("#")]
|
|
||||||
|
|
||||||
model = Model("openai/Qwen3-Coder-Next")
|
|
||||||
io = InputOutput(yes=True) # alle Rueckfragen automatisch bejahen
|
|
||||||
coder = Coder.create(
|
|
||||||
main_model=model,
|
|
||||||
io=io,
|
|
||||||
fnames=["SAVEPOINT.md"], # editierbar
|
|
||||||
read_only_fnames=["CONVENTIONS.md"], # nur-lesbar
|
|
||||||
auto_commits=True, # jede Etappe -> git-Commit
|
|
||||||
stream=False, # deterministische Logs fuer P0
|
|
||||||
map_tokens=512,
|
|
||||||
use_git=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Aiders eigene Zusammenfassung ausschalten: too_big() wird nie wahr.
|
|
||||||
try:
|
|
||||||
coder.summarizer.max_tokens = 10 ** 9
|
|
||||||
print(f"[driver] summarizer.max_tokens -> {coder.summarizer.max_tokens}", flush=True)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"[driver] WARN konnte summarizer nicht abschalten: {exc}", flush=True)
|
|
||||||
|
|
||||||
for i, msg in enumerate(messages, 1):
|
|
||||||
print(f"\n===== TURN {i}/{len(messages)} =====", flush=True)
|
|
||||||
print(f">> {msg[:140]}", flush=True)
|
|
||||||
try:
|
|
||||||
coder.run(with_message=msg)
|
|
||||||
except Exception as exc:
|
|
||||||
print(f"[driver] TURN {i} Fehler: {exc!r}", flush=True)
|
|
||||||
break
|
|
||||||
sent = getattr(coder, "total_tokens_sent", "?")
|
|
||||||
recv = getattr(coder, "total_tokens_received", "?")
|
|
||||||
print(f"-- aider kum: gesendet={sent} empfangen={recv}", flush=True)
|
|
||||||
|
|
||||||
print("\n===== SESSION ENDE =====", flush=True)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# gov-ctl.sh — Governor sauber starten/stoppen (Phase-0-Helfer).
|
|
||||||
# Nutzung:
|
|
||||||
# gov-ctl.sh start [SCHWELLE] # startet detached, Default-Schwelle 25000
|
|
||||||
# gov-ctl.sh stop
|
|
||||||
# gov-ctl.sh status
|
|
||||||
set -u
|
|
||||||
DIR="$HOME/governor-p0"
|
|
||||||
PIDF="$DIR/governor.pid"
|
|
||||||
LOG="$DIR/governor.log"
|
|
||||||
OUT="$DIR/governor.stdout"
|
|
||||||
|
|
||||||
start() {
|
|
||||||
stop
|
|
||||||
local thr="${1:-25000}"
|
|
||||||
cd "$DIR"
|
|
||||||
: > "$LOG"
|
|
||||||
# Voll detachen: eigene Session, alle FDs weg vom Aufrufer.
|
|
||||||
# Hart-Deckel nur setzen, wenn explizit uebergeben (Arg 2); sonst rechnet
|
|
||||||
# governor.py den Default (Soft + 5000). CPT-Default 3.5 (kalibriert 24.07.).
|
|
||||||
local hardenv=()
|
|
||||||
if [ -n "${2:-}" ]; then hardenv=(GOV_HARD_CEILING="$2"); fi
|
|
||||||
setsid env GOV_THRESHOLD="$thr" "${hardenv[@]}" \
|
|
||||||
GOV_CHARS_PER_TOKEN="${GOV_CHARS_PER_TOKEN:-3.5}" \
|
|
||||||
GOV_LOG="$LOG" GOV_PORT=8100 \
|
|
||||||
python3 "$DIR/governor.py" </dev/null >>"$OUT" 2>&1 &
|
|
||||||
echo $! > "$PIDF"
|
|
||||||
sleep 1.2
|
|
||||||
echo "started pid=$(cat "$PIDF") threshold=$thr hard=${2:-auto}"
|
|
||||||
ss -tlnp 2>/dev/null | grep ":8100" >/dev/null && echo "listening :8100 OK" || echo "WARN: not listening"
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
pkill -f "$DIR/governor.py" 2>/dev/null || true
|
|
||||||
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null || true
|
|
||||||
sleep 0.6
|
|
||||||
rm -f "$PIDF"
|
|
||||||
}
|
|
||||||
|
|
||||||
status() {
|
|
||||||
if pgrep -f "$DIR/governor.py" >/dev/null; then
|
|
||||||
echo "running pid=$(pgrep -f "$DIR/governor.py" | tr '\n' ' ')"
|
|
||||||
ss -tlnp 2>/dev/null | grep ":8100" || true
|
|
||||||
else
|
|
||||||
echo "not running"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
case "${1:-status}" in
|
|
||||||
start) shift; start "${1:-25000}" "${2:-}" ;;
|
|
||||||
stop) stop; echo stopped ;;
|
|
||||||
status) status ;;
|
|
||||||
*) echo "usage: gov-ctl.sh {start [soft] [hart]|stop|status}"; exit 2 ;;
|
|
||||||
esac
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Governor v2 — Token-Waechter-Proxy vor dem MC2-Gateway.
|
|
||||||
|
|
||||||
Sitzt zwischen den Coding-Agenten (OpenCode/Zed, Nacht-Laeufe, Hermes-Worker) und dem
|
|
||||||
MC2-Gateway (:9001). Reicht ALLES unveraendert durch — mit einer Ausnahme bei
|
|
||||||
/v1/chat/completions: er bestimmt die Groesse der Anfrage (= Sitzungsgroesse, weil die
|
|
||||||
ganze Historie jede Runde mitkommt) und handelt nach zwei Schwellen:
|
|
||||||
|
|
||||||
est >= SOFT: haengt eine Stopp-Anweisung als letzte User-Nachricht an ("SAVEPOINT.md
|
|
||||||
finalisieren + stoppen") und leitet weiter. Loggt FIRED.
|
|
||||||
est >= HART: antwortet SELBST mit einer kurzen Stopp-Nachricht, OHNE das Modell zu
|
|
||||||
fragen. Verhindert Fassaden jenseits der Grenze. Loggt HARDSTOP.
|
|
||||||
|
|
||||||
--- Was v2 gegenueber v0.2 aendert (25.07.2026) ---------------------------------------
|
|
||||||
1. EHRLICH ZAEHLEN. v0.2 zaehlte nur Text in `messages` und ignorierte `tools`/
|
|
||||||
`tool_calls`. Bei werkzeugdichten Agenten lag es um Faktor 3 daneben (gemessen im
|
|
||||||
eigenen Log: est=3353 exact=10224). v2 zaehlt den GANZEN Anfragekoerper — inklusive
|
|
||||||
Werkzeug-Schemata, Werkzeug-Aufrufe und Werkzeug-Ergebnisse.
|
|
||||||
2. SELBST-KALIBRIERUNG. Aus jeder Antwort liest der Governor die echten
|
|
||||||
`usage.prompt_tokens` und korrigiert damit sein Zeichen-pro-Token-Verhaeltnis —
|
|
||||||
pro Modell, gleitend. Die Schaetzung wird also im Betrieb immer genauer, statt auf
|
|
||||||
einem einmal geratenen Wert festzuhaengen.
|
|
||||||
3. TOOL-CALL-SICHERER EINSCHUB. Der Soft-Einschub wird NUR angehaengt, wenn die
|
|
||||||
Nachrichtenkette das erlaubt (letzte Nachricht ist nicht ein Assistant mit offenen
|
|
||||||
tool_calls und keine tool-Antwort). Sonst wartet er auf die naechste Runde. Ohne
|
|
||||||
diese Pruefung zerbricht der Einschub bei OpenCode die Werkzeug-Reihenfolge.
|
|
||||||
4. STATUS-ENDPUNKT. GET /governor/status liefert Zaehlerstand, Kalibrierung und die
|
|
||||||
letzten Laeufe als JSON — Datenquelle fuer die MC2-Oberflaeche, das OpenCode-Plugin
|
|
||||||
und Lucys `loop_status`.
|
|
||||||
|
|
||||||
Bewusst nur Standardbibliothek: kein pip, kein venv, laeuft mit System-python3.
|
|
||||||
Bewusst ohne Datenbank: ein kleiner Ring im Speicher, mehr braucht es nicht.
|
|
||||||
|
|
||||||
Konfiguration per Umgebungsvariablen (alle optional):
|
|
||||||
GOV_PORT Listen-Port (Default 8100)
|
|
||||||
GOV_HOST Listen-Adresse (Default 0.0.0.0)
|
|
||||||
GOV_UPSTREAM Ziel (Default http://127.0.0.1:9001)
|
|
||||||
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 45000)
|
|
||||||
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default: Soft+5000, AN)
|
|
||||||
GOV_CHARS_PER_TOKEN Startwert Zeichen->Token (Default 3.2, danach gelernt)
|
|
||||||
GOV_CALIBRATE Selbst-Kalibrierung an/aus (Default 1)
|
|
||||||
GOV_LOG Logdatei (zusaetzlich zu stdout) (Default ./governor.log)
|
|
||||||
GOV_DIRECTIVE Text des Soft-Einschubs
|
|
||||||
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort
|
|
||||||
GOV_ANNOUNCE_URL Lucy-Sprach-Signal; "" = aus (Default :9001/api/voice/announce)
|
|
||||||
GOV_ANNOUNCE_THROTTLE Sekunden zwischen Signalen (Default 300)
|
|
||||||
GOV_ANNOUNCE_TEXT Text des Sprach-Signals
|
|
||||||
GOV_EXEMPT_MODELS Modelle ohne Schnitt, kommasepariert (Default: hermes,fast,embed,
|
|
||||||
reranker,vision,scout — Lucys Alltag wird nie unterbrochen)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import http.client
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from collections import deque
|
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
# ---- Konfiguration ---------------------------------------------------------
|
|
||||||
|
|
||||||
PORT = int(os.environ.get("GOV_PORT", "8100"))
|
|
||||||
HOST = os.environ.get("GOV_HOST", "0.0.0.0")
|
|
||||||
# Ziel ist das MC2-Gateway, NICHT llama-swap direkt: so bleiben MC2s Rollen-Aliase,
|
|
||||||
# Bild-Weiche und Telemetrie erhalten. Der Governor ist eine Schicht davor, kein Ersatz.
|
|
||||||
UPSTREAM = os.environ.get("GOV_UPSTREAM", "http://127.0.0.1:9001")
|
|
||||||
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "45000"))
|
|
||||||
_hard_env = os.environ.get("GOV_HARD_CEILING")
|
|
||||||
HARD_CEILING = (THRESHOLD + 5000) if _hard_env is None else int(_hard_env)
|
|
||||||
CHARS_PER_TOKEN = float(os.environ.get("GOV_CHARS_PER_TOKEN", "3.2"))
|
|
||||||
CALIBRATE = os.environ.get("GOV_CALIBRATE", "1") != "0"
|
|
||||||
LOG_PATH = os.environ.get("GOV_LOG", os.path.join(os.getcwd(), "governor.log"))
|
|
||||||
|
|
||||||
# Lucys Alltagsmodelle bekommen NIE einen Savepoint-Einschub: sie fuehren Gespraeche,
|
|
||||||
# keine Bau-Sitzungen. Nur die Coding-Rollen laufen gegen die Schwelle.
|
|
||||||
_DEFAULT_EXEMPT = "hermes,fast,embed,reranker,vision,scout"
|
|
||||||
EXEMPT_MODELS = {m.strip().lower() for m in
|
|
||||||
os.environ.get("GOV_EXEMPT_MODELS", _DEFAULT_EXEMPT).split(",") if m.strip()}
|
|
||||||
|
|
||||||
DEFAULT_DIRECTIVE = (
|
|
||||||
"[GOVERNOR — SITZUNGS-LIMIT ERREICHT] Der Kontext dieser Sitzung ist auf ~{est} "
|
|
||||||
"Tokens gewachsen (Limit {threshold}). Beginne oder setze JETZT KEINE weiteren "
|
|
||||||
"Code-Aenderungen fort. Stattdessen, in dieser Reihenfolge:\n"
|
|
||||||
"1. Aktualisiere SAVEPOINT.md so, dass es den aktuellen Stand vollstaendig festhaelt: "
|
|
||||||
"was WIRKLICH erledigt ist (nur was im Code steht — nichts aus Absicht oder git-"
|
|
||||||
"Nachrichten ableiten), der genaue naechste Schritt, offene Fragen und alle "
|
|
||||||
"Stolpersteine — genug, dass eine frische Sitzung ohne jede Erinnerung allein aus "
|
|
||||||
"SAVEPOINT.md plus git-Historie sauber weitermachen kann.\n"
|
|
||||||
"2. Halte dann an und sage dem Nutzer in einem Satz, dass er eine frische Sitzung "
|
|
||||||
"starten soll. Gib ausser der SAVEPOINT.md-Aktualisierung und diesem Hinweis nichts aus."
|
|
||||||
)
|
|
||||||
DIRECTIVE = os.environ.get("GOV_DIRECTIVE", DEFAULT_DIRECTIVE)
|
|
||||||
|
|
||||||
DEFAULT_HARDSTOP = (
|
|
||||||
"[GOVERNOR — HARTER STOPP] Das Sitzungs-Limit ist ueberschritten und der Savepoint "
|
|
||||||
"sollte bereits finalisiert sein. Diese Sitzung nimmt keine weiteren Auftraege mehr an. "
|
|
||||||
"Bitte starte eine FRISCHE Sitzung — sie liest SAVEPOINT.md und die git-Historie und "
|
|
||||||
"macht sauber weiter. (Keine Code-Aenderung in dieser Antwort.)"
|
|
||||||
)
|
|
||||||
HARDSTOP_MSG = os.environ.get("GOV_HARDSTOP_MSG", DEFAULT_HARDSTOP)
|
|
||||||
|
|
||||||
ANNOUNCE_URL = os.environ.get("GOV_ANNOUNCE_URL", "http://127.0.0.1:9001/api/voice/announce")
|
|
||||||
ANNOUNCE_THROTTLE = float(os.environ.get("GOV_ANNOUNCE_THROTTLE", "300"))
|
|
||||||
DEFAULT_ANNOUNCE = (
|
|
||||||
"Commander, die Coding-Sitzung wird voll — ungefähr {est} Tokens. Ich sichere den "
|
|
||||||
"Stand im Savepoint; am besten fangen wir gleich frisch an."
|
|
||||||
)
|
|
||||||
ANNOUNCE_TEXT = os.environ.get("GOV_ANNOUNCE_TEXT", DEFAULT_ANNOUNCE)
|
|
||||||
|
|
||||||
up = urlparse(UPSTREAM)
|
|
||||||
UP_HOST = up.hostname or "127.0.0.1"
|
|
||||||
UP_PORT = up.port or 80
|
|
||||||
|
|
||||||
HOP_BY_HOP = {
|
|
||||||
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
|
||||||
"te", "trailers", "transfer-encoding", "upgrade",
|
|
||||||
}
|
|
||||||
|
|
||||||
_log_lock = threading.Lock()
|
|
||||||
_PROMPT_TOKENS_RE = re.compile(r'"prompt_tokens"\s*:\s*(\d+)')
|
|
||||||
_announce_lock = threading.Lock()
|
|
||||||
_last_announce = 0.0
|
|
||||||
_an = urlparse(ANNOUNCE_URL) if ANNOUNCE_URL else None
|
|
||||||
|
|
||||||
# ---- Zustand (klein, im Speicher) ------------------------------------------
|
|
||||||
# Kalibrierung je Modell: gleitender Mittelwert von zeichen/echte_tokens. Startwert ist
|
|
||||||
# GOV_CHARS_PER_TOKEN; jede Antwort mit usage zieht ihn Richtung Wahrheit.
|
|
||||||
_state_lock = threading.Lock()
|
|
||||||
_cpt: dict = {} # modell -> gelerntes Zeichen-pro-Token
|
|
||||||
_cpt_n: dict = {} # modell -> Anzahl Messungen
|
|
||||||
_recent: deque = deque(maxlen=50) # letzte Laeufe fuer /governor/status
|
|
||||||
_counters = {"chat": 0, "soft": 0, "hard": 0, "passthrough": 0,
|
|
||||||
"tokens_prompt": 0, "tokens_completion": 0, "started": time.time()}
|
|
||||||
|
|
||||||
CPT_MIN, CPT_MAX = 0.8, 8.0 # Schutz gegen Ausreisser
|
|
||||||
|
|
||||||
|
|
||||||
def log(line: str) -> None:
|
|
||||||
"""Eine Zeile nach stdout UND in die Logdatei (thread-sicher)."""
|
|
||||||
stamp = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
||||||
msg = f"{stamp} {line}"
|
|
||||||
with _log_lock:
|
|
||||||
print(msg, flush=True)
|
|
||||||
try:
|
|
||||||
with open(LOG_PATH, "a", encoding="utf-8") as fh:
|
|
||||||
fh.write(msg + "\n")
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def cpt_for(model: str) -> float:
|
|
||||||
"""Aktuelles Zeichen-pro-Token-Verhaeltnis fuer ein Modell (gelernt oder Startwert)."""
|
|
||||||
with _state_lock:
|
|
||||||
return _cpt.get(model, CHARS_PER_TOKEN)
|
|
||||||
|
|
||||||
|
|
||||||
def calibrate(model: str, chars: int, exact: int) -> None:
|
|
||||||
"""Aus einer echten Antwort lernen. Gleitender Mittelwert mit sanftem Gewicht —
|
|
||||||
ein einzelner Ausreisser (z. B. ein riesiges Bild) verbiegt nichts."""
|
|
||||||
if not CALIBRATE or not exact or exact <= 0 or chars <= 0:
|
|
||||||
return
|
|
||||||
ratio = chars / exact
|
|
||||||
if not (CPT_MIN <= ratio <= CPT_MAX):
|
|
||||||
return
|
|
||||||
with _state_lock:
|
|
||||||
n = _cpt_n.get(model, 0)
|
|
||||||
old = _cpt.get(model, CHARS_PER_TOKEN)
|
|
||||||
# Gewicht faellt mit der Anzahl Messungen: schnell einschwingen, dann stabil.
|
|
||||||
w = max(0.08, 1.0 / (n + 2))
|
|
||||||
_cpt[model] = old * (1 - w) + ratio * w
|
|
||||||
_cpt_n[model] = n + 1
|
|
||||||
|
|
||||||
|
|
||||||
def body_chars(data: dict) -> int:
|
|
||||||
"""Zeichen des GESAMTEN Anfragekoerpers — der Kern der Ehrlichkeit.
|
|
||||||
|
|
||||||
v0.2 zaehlte nur Text in `messages` und lag bei werkzeugdichten Agenten um Faktor 3
|
|
||||||
daneben, weil Werkzeug-Schemata (`tools`), Werkzeug-Aufrufe (`tool_calls`) und
|
|
||||||
Werkzeug-Ergebnisse mitgeschickt werden und im Kontext genauso Platz fressen.
|
|
||||||
Wir serialisieren einfach alles, was ans Modell geht.
|
|
||||||
"""
|
|
||||||
payload = {k: v for k, v in data.items()
|
|
||||||
if k in ("messages", "tools", "tool_choice", "system", "functions")}
|
|
||||||
try:
|
|
||||||
return len(json.dumps(payload, ensure_ascii=False))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
# Fallback: nur Nachrichtentext (nie schlechter als v0.2)
|
|
||||||
chars = 0
|
|
||||||
for m in data.get("messages") or []:
|
|
||||||
c = m.get("content") if isinstance(m, dict) else None
|
|
||||||
if isinstance(c, str):
|
|
||||||
chars += len(c) + 4
|
|
||||||
elif isinstance(c, list):
|
|
||||||
for p in c:
|
|
||||||
if isinstance(p, dict) and isinstance(p.get("text"), str):
|
|
||||||
chars += len(p["text"])
|
|
||||||
return chars
|
|
||||||
|
|
||||||
|
|
||||||
def safe_to_append(messages) -> bool:
|
|
||||||
"""Darf der Soft-Einschub JETZT als user-Nachricht ans Ende?
|
|
||||||
|
|
||||||
Nein, wenn die Kette gerade mitten in einem Werkzeug-Austausch steckt: nach einem
|
|
||||||
Assistant mit offenen `tool_calls` MUSS eine `tool`-Antwort folgen — schiebt man da
|
|
||||||
eine user-Nachricht dazwischen, lehnt das Modell (bzw. das Template) die Anfrage ab
|
|
||||||
oder halluziniert. Dann warten wir einfach auf die naechste Runde; die Schwelle ist
|
|
||||||
ohnehin ueberschritten, es kommt in Sekunden ein neuer Zug.
|
|
||||||
"""
|
|
||||||
if not isinstance(messages, list) or not messages:
|
|
||||||
return False
|
|
||||||
last = messages[-1]
|
|
||||||
if not isinstance(last, dict):
|
|
||||||
return False
|
|
||||||
role = last.get("role")
|
|
||||||
if role == "tool":
|
|
||||||
return False
|
|
||||||
return not (role == "assistant" and last.get("tool_calls"))
|
|
||||||
|
|
||||||
|
|
||||||
def _post_announce(est) -> None:
|
|
||||||
"""POSTet die Meldung an die MC2-Announce-Pipeline (Lucy spricht sie)."""
|
|
||||||
try:
|
|
||||||
text = (ANNOUNCE_TEXT.replace("{est}", str(est))
|
|
||||||
.replace("{threshold}", str(THRESHOLD)))
|
|
||||||
body = json.dumps({"text": text, "subject": "[Governor]",
|
|
||||||
"source": "governor", "priority": "normal"}).encode("utf-8")
|
|
||||||
conn = http.client.HTTPConnection(_an.hostname or "127.0.0.1",
|
|
||||||
_an.port or 80, timeout=4)
|
|
||||||
conn.request("POST", _an.path or "/api/voice/announce", body=body,
|
|
||||||
headers={"Content-Type": "application/json",
|
|
||||||
"Content-Length": str(len(body))})
|
|
||||||
resp = conn.getresponse()
|
|
||||||
resp.read()
|
|
||||||
conn.close()
|
|
||||||
log(f"ANNOUNCE -> Lucy status={resp.status} est={est}")
|
|
||||||
except Exception as exc:
|
|
||||||
log(f"ANNOUNCE fehlgeschlagen: {exc!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def maybe_announce(est) -> None:
|
|
||||||
"""Sprach-Signal an Lucy — gedrosselt (eine Aeusserung je Episode)."""
|
|
||||||
if not _an:
|
|
||||||
return
|
|
||||||
global _last_announce
|
|
||||||
now = time.time()
|
|
||||||
with _announce_lock:
|
|
||||||
if now - _last_announce < ANNOUNCE_THROTTLE:
|
|
||||||
return
|
|
||||||
_last_announce = now
|
|
||||||
threading.Thread(target=_post_announce, args=(est,), daemon=True).start()
|
|
||||||
|
|
||||||
|
|
||||||
def status_payload() -> dict:
|
|
||||||
"""Momentaufnahme fuer /governor/status (MC2-Oberflaeche, Plugin, Lucy)."""
|
|
||||||
with _state_lock:
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"upstream": UPSTREAM,
|
|
||||||
"soft": THRESHOLD,
|
|
||||||
"hard": HARD_CEILING if HARD_CEILING > 0 else None,
|
|
||||||
"uptime_s": int(time.time() - _counters["started"]),
|
|
||||||
"counters": {k: v for k, v in _counters.items() if k != "started"},
|
|
||||||
"calibration": {m: {"chars_per_token": round(v, 3), "samples": _cpt_n.get(m, 0)}
|
|
||||||
for m, v in _cpt.items()},
|
|
||||||
"calibration_default": CHARS_PER_TOKEN,
|
|
||||||
"exempt_models": sorted(EXEMPT_MODELS),
|
|
||||||
"recent": list(_recent),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
|
||||||
protocol_version = "HTTP/1.1"
|
|
||||||
server_version = "Governor/2.0"
|
|
||||||
|
|
||||||
def log_message(self, *args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path.split("?", 1)[0].rstrip("/") in ("/governor/status", "/governor"):
|
|
||||||
self._send_json(200, status_payload())
|
|
||||||
return
|
|
||||||
self._proxy()
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
self._proxy()
|
|
||||||
|
|
||||||
def do_PUT(self):
|
|
||||||
self._proxy()
|
|
||||||
|
|
||||||
def do_DELETE(self):
|
|
||||||
self._proxy()
|
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
|
||||||
self._proxy()
|
|
||||||
|
|
||||||
# -- Kern ---------------------------------------------------------------
|
|
||||||
def _send_json(self, status: int, obj) -> None:
|
|
||||||
try:
|
|
||||||
data = json.dumps(obj).encode("utf-8")
|
|
||||||
self.send_response(status)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
|
||||||
self.send_header("Content-Length", str(len(data)))
|
|
||||||
self.send_header("Connection", "close")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(data)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _read_body(self) -> bytes:
|
|
||||||
length = self.headers.get("Content-Length")
|
|
||||||
if length is None:
|
|
||||||
return b""
|
|
||||||
try:
|
|
||||||
return self.rfile.read(int(length))
|
|
||||||
except (ValueError, OSError):
|
|
||||||
return b""
|
|
||||||
|
|
||||||
def _proxy(self) -> None:
|
|
||||||
body = self._read_body()
|
|
||||||
path = self.path
|
|
||||||
clean_path = path.split("?", 1)[0]
|
|
||||||
is_chat = clean_path.rstrip("/").endswith("/chat/completions")
|
|
||||||
|
|
||||||
action = "passthrough"
|
|
||||||
est = None
|
|
||||||
streaming = False
|
|
||||||
model = ""
|
|
||||||
chars = 0
|
|
||||||
if is_chat and body:
|
|
||||||
action, body, est, streaming, model, chars = self._decide(body)
|
|
||||||
if action in ("soft", "hard"):
|
|
||||||
maybe_announce(est)
|
|
||||||
|
|
||||||
if action == "hard":
|
|
||||||
self._send_canned_stop(model, streaming, est)
|
|
||||||
with _state_lock:
|
|
||||||
_counters["chat"] += 1
|
|
||||||
_counters["hard"] += 1
|
|
||||||
_recent.appendleft({"t": int(time.time()), "model": model, "est": est,
|
|
||||||
"exact": None, "action": "hard"})
|
|
||||||
log(f"chat model={model} est={est} thr={THRESHOLD} hard={HARD_CEILING} "
|
|
||||||
f"HARDSTOP stream={streaming} status=200")
|
|
||||||
return
|
|
||||||
|
|
||||||
out_headers = {}
|
|
||||||
for k, v in self.headers.items():
|
|
||||||
kl = k.lower()
|
|
||||||
if kl in HOP_BY_HOP or kl in ("host", "content-length", "accept-encoding"):
|
|
||||||
continue
|
|
||||||
out_headers[k] = v
|
|
||||||
out_headers["Host"] = f"{UP_HOST}:{UP_PORT}"
|
|
||||||
out_headers["Accept-Encoding"] = "identity"
|
|
||||||
if body:
|
|
||||||
out_headers["Content-Length"] = str(len(body))
|
|
||||||
out_headers["Connection"] = "close"
|
|
||||||
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
conn = http.client.HTTPConnection(UP_HOST, UP_PORT, timeout=900)
|
|
||||||
conn.request(self.command, path, body=body or None, headers=out_headers)
|
|
||||||
resp = conn.getresponse()
|
|
||||||
except (OSError, http.client.HTTPException) as exc:
|
|
||||||
log(f"ERROR upstream {self.command} {path}: {exc!r}")
|
|
||||||
if conn is not None:
|
|
||||||
conn.close()
|
|
||||||
self._safe_error(502, f"governor upstream: {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.send_response(resp.status)
|
|
||||||
for k, v in resp.getheaders():
|
|
||||||
kl = k.lower()
|
|
||||||
if kl in HOP_BY_HOP or kl in ("content-length", "date", "server"):
|
|
||||||
continue
|
|
||||||
self.send_header(k, v)
|
|
||||||
self.send_header("Connection", "close")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
tail = bytearray()
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
# read1() gibt jedes Upstream-Stueck sofort zurueck (echtes SSE-
|
|
||||||
# Durchreichen); read() wuerde puffern und Streaming haengen lassen.
|
|
||||||
chunk = resp.read1(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
self.wfile.write(chunk)
|
|
||||||
self.wfile.flush()
|
|
||||||
tail.extend(chunk)
|
|
||||||
if len(tail) > 16384:
|
|
||||||
del tail[:-16384]
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
exact = self._scan_prompt_tokens(tail)
|
|
||||||
if is_chat:
|
|
||||||
if exact:
|
|
||||||
calibrate(model, chars, exact)
|
|
||||||
with _state_lock:
|
|
||||||
_counters["chat"] += 1
|
|
||||||
_counters["soft" if action == "soft" else "passthrough"] += 1
|
|
||||||
if exact:
|
|
||||||
_counters["tokens_prompt"] += exact
|
|
||||||
_recent.appendleft({"t": int(time.time()), "model": model, "est": est,
|
|
||||||
"exact": exact, "action": action})
|
|
||||||
exact_s = str(exact) if exact is not None else "-"
|
|
||||||
flag = "FIRED" if action == "soft" else ("skip" if action == "defer" else "ok")
|
|
||||||
log(f"chat model={model} est={est} exact={exact_s} cpt={cpt_for(model):.2f} "
|
|
||||||
f"thr={THRESHOLD} {flag} stream={streaming} status={resp.status}")
|
|
||||||
|
|
||||||
def _decide(self, body: bytes):
|
|
||||||
"""Aktion bestimmen. Rueckgabe: (action, body, est, streaming, model, chars)."""
|
|
||||||
try:
|
|
||||||
data = json.loads(body)
|
|
||||||
except (ValueError, UnicodeDecodeError):
|
|
||||||
return "passthrough", body, None, False, "", 0
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return "passthrough", body, None, False, "", 0
|
|
||||||
|
|
||||||
messages = data.get("messages")
|
|
||||||
streaming = bool(data.get("stream"))
|
|
||||||
model = (data.get("model") or "").strip()
|
|
||||||
chars = body_chars(data)
|
|
||||||
est = int(chars / max(cpt_for(model), 0.1))
|
|
||||||
|
|
||||||
# Lucys Alltagsmodelle laufen nie gegen die Schwelle — ein Gespraech ist keine
|
|
||||||
# Bau-Sitzung. Wir zaehlen sie trotzdem mit (Kalibrierung + Telemetrie).
|
|
||||||
base = model.split("/")[-1].lower()
|
|
||||||
if base in EXEMPT_MODELS:
|
|
||||||
return "passthrough", body, est, streaming, model, chars
|
|
||||||
|
|
||||||
if not isinstance(messages, list):
|
|
||||||
return "passthrough", body, est, streaming, model, chars
|
|
||||||
|
|
||||||
has_warned = False
|
|
||||||
marker = "[GOVERNOR — SITZUNGS-LIMIT ERREICHT]"
|
|
||||||
for m in reversed(messages):
|
|
||||||
if m.get("role") == "user" and isinstance(m.get("content"), str) and marker in m["content"]:
|
|
||||||
has_warned = True
|
|
||||||
break
|
|
||||||
|
|
||||||
is_safe = safe_to_append(messages)
|
|
||||||
|
|
||||||
# 1. Absolutes Not-Aus bei komplettem Amoklauf
|
|
||||||
if HARD_CEILING > 0 and est >= HARD_CEILING + 10000:
|
|
||||||
return "hard", body, est, streaming, model, chars
|
|
||||||
|
|
||||||
# 2. Sind wir am Limit?
|
|
||||||
if est >= THRESHOLD:
|
|
||||||
if not is_safe:
|
|
||||||
# Agent arbeitet gerade an einer Tool-Kette. Auf keinen Fall abbrechen!
|
|
||||||
return "defer", body, est, streaming, model, chars
|
|
||||||
|
|
||||||
# Es ist sicher (Tool-Kette beendet oder Agent wartet auf Input).
|
|
||||||
if HARD_CEILING > 0 and est >= HARD_CEILING:
|
|
||||||
if has_warned:
|
|
||||||
# Wir haben ihn schon gewarnt, er macht trotzdem weiter -> harter Schnitt.
|
|
||||||
return "hard", body, est, streaming, model, chars
|
|
||||||
else:
|
|
||||||
# Er hat wegen einer langen Tool-Kette direkt das Hard-Limit ueberschritten.
|
|
||||||
# Gib ihm trotzdem noch den einen Finalisier-Zug (Soft).
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Soft-Limit greift
|
|
||||||
if not has_warned:
|
|
||||||
directive = (DIRECTIVE.replace("{est}", str(est))
|
|
||||||
.replace("{threshold}", str(THRESHOLD)))
|
|
||||||
messages.append({"role": "user", "content": directive})
|
|
||||||
data["messages"] = messages
|
|
||||||
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
|
|
||||||
|
|
||||||
return "passthrough", body, est, streaming, model, chars
|
|
||||||
|
|
||||||
def _send_canned_stop(self, model: str, streaming: bool, est) -> None:
|
|
||||||
"""OpenAI-kompatible Stopp-Antwort selbst erzeugen (kein Upstream-Call)."""
|
|
||||||
created = int(time.time())
|
|
||||||
usage = {"prompt_tokens": est or 0, "completion_tokens": 0,
|
|
||||||
"total_tokens": est or 0}
|
|
||||||
try:
|
|
||||||
if streaming:
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "text/event-stream")
|
|
||||||
self.send_header("Cache-Control", "no-cache")
|
|
||||||
self.send_header("Connection", "close")
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def sse(obj):
|
|
||||||
self.wfile.write(b"data: " + json.dumps(obj).encode() + b"\n\n")
|
|
||||||
self.wfile.flush()
|
|
||||||
|
|
||||||
base = {"id": "governor-hardstop", "object": "chat.completion.chunk",
|
|
||||||
"created": created, "model": model}
|
|
||||||
sse({**base, "choices": [{"index": 0, "delta": {"role": "assistant"},
|
|
||||||
"finish_reason": None}]})
|
|
||||||
sse({**base, "choices": [{"index": 0, "delta": {"content": HARDSTOP_MSG},
|
|
||||||
"finish_reason": None}]})
|
|
||||||
sse({**base, "choices": [{"index": 0, "delta": {},
|
|
||||||
"finish_reason": "stop"}], "usage": usage})
|
|
||||||
self.wfile.write(b"data: [DONE]\n\n")
|
|
||||||
self.wfile.flush()
|
|
||||||
else:
|
|
||||||
payload = {
|
|
||||||
"id": "governor-hardstop", "object": "chat.completion",
|
|
||||||
"created": created, "model": model,
|
|
||||||
"choices": [{"index": 0, "finish_reason": "stop",
|
|
||||||
"message": {"role": "assistant", "content": HARDSTOP_MSG}}],
|
|
||||||
"usage": usage,
|
|
||||||
}
|
|
||||||
data = json.dumps(payload).encode("utf-8")
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.send_header("Content-Length", str(len(data)))
|
|
||||||
self.send_header("Connection", "close")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(data)
|
|
||||||
self.wfile.flush()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _safe_error(self, status: int, msg: str) -> None:
|
|
||||||
self._send_json(status, {"error": msg})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _scan_prompt_tokens(tail: bytearray):
|
|
||||||
if not tail:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
text = tail.decode("utf-8", errors="ignore")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
matches = _PROMPT_TOKENS_RE.findall(text)
|
|
||||||
if not matches:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
return int(matches[-1])
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
log_dir = os.path.dirname(LOG_PATH)
|
|
||||||
if log_dir and not os.path.isdir(log_dir):
|
|
||||||
try:
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
|
||||||
server.daemon_threads = True
|
|
||||||
hard = HARD_CEILING if HARD_CEILING > 0 else "aus"
|
|
||||||
log(f"Governor v2 startet auf {HOST}:{PORT} -> {UPSTREAM} | Soft={THRESHOLD} "
|
|
||||||
f"Hart={hard} | CPT-Start={CHARS_PER_TOKEN} kalibrierend={CALIBRATE} | "
|
|
||||||
f"ausgenommen={sorted(EXEMPT_MODELS)} | Log={LOG_PATH}")
|
|
||||||
try:
|
|
||||||
server.serve_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log("Governor beendet (SIGINT).")
|
|
||||||
finally:
|
|
||||||
server.server_close()
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
# Governor v2 — Token-Waechter vor dem MC2-Gateway.
|
|
||||||
# Nutzer-Dienst (systemctl --user), weil er unter hitonabi laeuft und keine
|
|
||||||
# Root-Rechte braucht. Startet nach MC2, weil er dorthin weiterreicht.
|
|
||||||
Description=Governor v2 — Token-Waechter-Proxy (:8100 -> MC2 :9001)
|
|
||||||
After=network-online.target mission-control-2.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
WorkingDirectory=%h/governor
|
|
||||||
ExecStart=/usr/bin/python3 %h/governor/governor.py
|
|
||||||
Restart=always
|
|
||||||
RestartSec=3
|
|
||||||
|
|
||||||
# --- Schwellen -------------------------------------------------------------
|
|
||||||
# Soft 45k: OpenCode startet mit ~10-15k allein fuer Systemprompt + Werkzeug-
|
|
||||||
# Schemata; 25k (der alte Aider-Wert) haette schon nach wenigen Zuegen gefeuert.
|
|
||||||
# Hart = Soft+5000 (ein Finalisier-Zug Luft), Default des Programms.
|
|
||||||
Environment=GOV_THRESHOLD=45000
|
|
||||||
Environment=GOV_UPSTREAM=http://127.0.0.1:9001
|
|
||||||
Environment=GOV_LOG=%h/governor/governor.log
|
|
||||||
# Lucys Alltagsmodelle laufen nie gegen die Schwelle — ein Gespraech ist kein Bau.
|
|
||||||
Environment=GOV_EXEMPT_MODELS=hermes,fast,embed,reranker,vision,scout
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# hardstop-test.sh — prueft die drei Governor-Pfade mit direkten curls:
|
|
||||||
# klein -> passthrough (Modell antwortet normal)
|
|
||||||
# mittel -> soft-Einschub (Modell bekommt die Stopp-Anweisung, antwortet)
|
|
||||||
# gross -> HART-STOPP (Governor antwortet selbst, KEIN Modell-Call)
|
|
||||||
set -u
|
|
||||||
GOV="http://127.0.0.1:8100/v1/chat/completions"
|
|
||||||
|
|
||||||
python3 - <<'PY' > /tmp/gov_small.json
|
|
||||||
import json
|
|
||||||
print(json.dumps({"model":"Qwen3-Coder-Next","messages":[{"role":"user","content":"Reply with exactly: SMALL"}],"max_tokens":8,"stream":False}))
|
|
||||||
PY
|
|
||||||
python3 - <<'PY' > /tmp/gov_mid.json
|
|
||||||
import json
|
|
||||||
print(json.dumps({"model":"Qwen3-Coder-Next","messages":[{"role":"user","content":"BEGIN "+"lorem ipsum "*2500+" END"}],"max_tokens":40,"stream":False}))
|
|
||||||
PY
|
|
||||||
python3 - <<'PY' > /tmp/gov_big.json
|
|
||||||
import json
|
|
||||||
print(json.dumps({"model":"Qwen3-Coder-Next","messages":[{"role":"user","content":"x"*40000}],"max_tokens":16,"stream":False}))
|
|
||||||
PY
|
|
||||||
|
|
||||||
echo "=== TEST A: klein (passthrough) ==="
|
|
||||||
curl -s -m 60 "$GOV" -H "Content-Type: application/json" --data @/tmp/gov_small.json | jq -r '.choices[0].message.content'
|
|
||||||
|
|
||||||
echo "=== TEST B: mittel ~30k Zeichen (soft-Einschub, geht ans Modell) ==="
|
|
||||||
curl -s -m 120 "$GOV" -H "Content-Type: application/json" --data @/tmp/gov_mid.json | jq '{id, content: (.choices[0].message.content|.[0:80])}'
|
|
||||||
|
|
||||||
echo "=== TEST C: gross ~40k Zeichen (HART-STOPP, kein Modell-Call) ==="
|
|
||||||
curl -s -m 30 "$GOV" -H "Content-Type: application/json" --data @/tmp/gov_big.json | jq '{id, finish: .choices[0].finish_reason, content: (.choices[0].message.content|.[0:70]), usage}'
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Wegwerf-Aufgabe: kleine Todo-App, schrittweise. Jede Zeile = eine Runde.
|
|
||||||
Create todo.py with a TodoList class: add(text) appends a dict {"text": text, "done": False} to an internal list, and items() returns that list. Then update SAVEPOINT.md following our conventions.
|
|
||||||
Add complete(index) and remove(index) to TodoList, each with a bounds check that raises IndexError with a clear message when the index is out of range. Update SAVEPOINT.md.
|
|
||||||
Add save(path) and load(path) to TodoList that persist the items to and from a JSON file. Update SAVEPOINT.md.
|
|
||||||
Add pending_count() and completed_count() methods to TodoList. Update SAVEPOINT.md.
|
|
||||||
Create cli.py with an argparse command line interface exposing subcommands add, list, done, and rm that operate on a todos.json file via TodoList. Update SAVEPOINT.md.
|
|
||||||
Add a clear subcommand to cli.py that removes all completed items from todos.json. Update SAVEPOINT.md.
|
|
||||||
Create test_todo.py with unittest tests covering add, complete, remove, save, load, and the IndexError bounds checks. Update SAVEPOINT.md.
|
|
||||||
Add tests for pending_count and completed_count to test_todo.py. Update SAVEPOINT.md.
|
|
||||||
Create README.md documenting the CLI usage with a short example for each subcommand. Update SAVEPOINT.md.
|
|
||||||
Add type hints throughout todo.py and cli.py. Update SAVEPOINT.md.
|
|
||||||
Add an optional due date field (ISO date string) to each item and a due argument to TodoList.add and to the CLI add subcommand. Update SAVEPOINT.md.
|
|
||||||
Add an overdue subcommand to cli.py that lists items whose due date is before today. Update SAVEPOINT.md.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# run-aider-msg.sh <repo> <message> — EINE Aider-Runde via --message (sauberer,
|
|
||||||
# unterstuetzter Einzel-Schuss ohne Scripting-Reflexions-Haenger), durch den Governor.
|
|
||||||
# todo.py/cli.py als Lesekontext, damit das Modell die echte API sieht (kein Erfinden)
|
|
||||||
# und keine "Datei hinzufuegen?"-Reflexion ausloest.
|
|
||||||
set -u
|
|
||||||
REPO="${1:?repo dir}"
|
|
||||||
MSG="${2:?message}"
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
export OPENAI_API_BASE="http://127.0.0.1:8100/v1"
|
|
||||||
export OPENAI_API_KEY="dummy"
|
|
||||||
cd "$REPO"
|
|
||||||
aider \
|
|
||||||
--model openai/Qwen3-Coder-Next \
|
|
||||||
--no-check-update --no-show-model-warnings --no-analytics --yes-always \
|
|
||||||
--map-tokens 512 \
|
|
||||||
--read CONVENTIONS.md --read todo.py --read cli.py \
|
|
||||||
SAVEPOINT.md \
|
|
||||||
--message "$MSG"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# run-aider.sh <messages-file> — treibt EINE Aider-Sitzung durch den Governor.
|
|
||||||
# Jede Zeile der Nachrichtendatei = eine User-Runde; die Historie akkumuliert,
|
|
||||||
# sodass die Anfrage jede Runde wächst (genau das prüft der Governor).
|
|
||||||
set -u
|
|
||||||
MSGS="${1:?Nachrichtendatei angeben}"
|
|
||||||
export PATH="$HOME/.local/bin:$PATH"
|
|
||||||
export OPENAI_API_BASE="http://127.0.0.1:8100/v1"
|
|
||||||
export OPENAI_API_KEY="dummy"
|
|
||||||
cd "$HOME/governor-p0/testrepo"
|
|
||||||
aider \
|
|
||||||
--model openai/Qwen3-Coder-Next \
|
|
||||||
--no-check-update \
|
|
||||||
--no-show-model-warnings \
|
|
||||||
--no-analytics \
|
|
||||||
--yes-always \
|
|
||||||
--map-tokens 512 \
|
|
||||||
--read CONVENTIONS.md \
|
|
||||||
SAVEPOINT.md \
|
|
||||||
< "$MSGS"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# run-driver.sh <messages-file> — Aider-Scripting-Sitzung durch den Governor.
|
|
||||||
set -u
|
|
||||||
MSGS="${1:?Nachrichtendatei angeben}"
|
|
||||||
REPO="${2:-$HOME/governor-p0/testrepo}"
|
|
||||||
VENV_PY="$HOME/.local/share/uv/tools/aider-chat/bin/python"
|
|
||||||
cd "$REPO"
|
|
||||||
exec "$VENV_PY" "$HOME/governor-p0/driver.py" "$MSGS"
|
|
||||||
@@ -1,28 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
# Post-Update-Check nach einem Hermes-Agent-Update: läuft unser geteiltes „Gehirn"
|
||||||
# (Mem0 + die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
# (die mc2-memory-Integration) noch? Exit 0 = alles ok, sonst 1 → der Job wird
|
||||||
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
# im UI rot, damit ein kaputtes Gehirn sofort auffällt.
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
MC_URL="${MC_URL:-http://127.0.0.1:9001}"
|
||||||
MEM0_URL="${MEM0_SERVICE_URL:-http://127.0.0.1:8765}"
|
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
echo "=== Hermes Post-Update: Gehirn-Check ==="
|
||||||
|
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -sf -m 8 "$MC_URL/api/memory" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · /api/memory antwortet"
|
|
||||||
else
|
|
||||||
echo "FAIL · /api/memory antwortet nicht"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
if grep -qE "^[[:space:]]*provider:[[:space:]]*'?mc2-memory'?" "$HERMES/config.yaml" 2>/dev/null \
|
||||||
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
&& grep -qE "memory_enabled:[[:space:]]*true" "$HERMES/config.yaml" 2>/dev/null; then
|
||||||
echo "PASS · memory.provider=mc2-memory aktiv"
|
echo "PASS · memory.provider=mc2-memory aktiv"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ models:
|
|||||||
# 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
|
||||||
# Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.)
|
# Warm-Set-Diät 07.07. ist RAM nicht mehr der Engpass.)
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 3
|
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-dflash --spec-draft-model /srv/models/Qwen3.6-35B-A3B-DFlash-GGUF/giocom-Qwen3.6-35B-A3B-DFlash-Q8_0.gguf
|
||||||
ttl: 0
|
ttl: 0
|
||||||
aliases:
|
aliases:
|
||||||
- hermes
|
- hermes
|
||||||
@@ -21,17 +21,33 @@ models:
|
|||||||
out: [text]
|
out: [text]
|
||||||
tools: true
|
tools: true
|
||||||
context: 65536
|
context: 65536
|
||||||
Qwen3-Coder-Next:
|
Qwen3.8-27B:
|
||||||
|
# Qwen 3.8 27B: Dichtes 27B-Modell mit hybrider Linear-Attention (48/64 Schichten linear),
|
||||||
|
# nativem Multimodal-Support (mmproj-BF16) und Tool-Calling via --jinja.
|
||||||
|
# Unser neuer Haupt-Coder mit 262k Kontext, Thinking Mode und extrem sauberer Code-Qualität.
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server -m /srv/models/Qwen3-Coder-Next-GGUF/Qwen3-Coder-Next-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --cache-reuse 256 -cram 16384
|
llama-server -m /srv/models/Qwen3.8-27B-GGUF/Qwen3.8-27B-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3.8-27B-GGUF/mmproj-BF16.gguf --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0
|
||||||
ttl: 600
|
ttl: 5400
|
||||||
aliases:
|
aliases:
|
||||||
- coder
|
- coder
|
||||||
capabilities:
|
capabilities:
|
||||||
in: [text]
|
in: [text, image]
|
||||||
out: [text]
|
out: [text]
|
||||||
tools: true
|
tools: true
|
||||||
context: 131072
|
context: 65536
|
||||||
|
Muse-Glimmer-30B:
|
||||||
|
# Meta Muse Glimmer 30B: Dichtes 30B-Agenten-Modell mit DFlash-Speculative-Drafting
|
||||||
|
# und 1.8B Perception Multimodal Projector. Ideal als Runtime-Debugger & Fehler-Diagnostiker.
|
||||||
|
cmd: |
|
||||||
|
llama-server -m /srv/models/Muse-Glimmer-30B-GGUF/Muse-Glimmer-30B-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Muse-Glimmer-30B-GGUF/mmproj-kquant.gguf --jinja --spec-type draft-dflash --spec-draft-model /srv/models/Muse-Glimmer-30B-GGUF/dflash-kquant.gguf
|
||||||
|
ttl: 600
|
||||||
|
aliases:
|
||||||
|
- debugger
|
||||||
|
capabilities:
|
||||||
|
in: [text, image]
|
||||||
|
out: [text]
|
||||||
|
tools: true
|
||||||
|
context: 65536
|
||||||
Qwen3-Embedding-0.6B:
|
Qwen3-Embedding-0.6B:
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server -m /srv/models/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf --host 127.0.0.1 --port ${PORT} --embedding --pooling last -ngl 999 -fa on --no-mmap -c 8192
|
llama-server -m /srv/models/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf --host 127.0.0.1 --port ${PORT} --embedding --pooling last -ngl 999 -fa on --no-mmap -c 8192
|
||||||
@@ -52,11 +68,6 @@ models:
|
|||||||
tools: true
|
tools: true
|
||||||
context: 32768
|
context: 32768
|
||||||
Qwen3-VL-30B-A3B-Instruct:
|
Qwen3-VL-30B-A3B-Instruct:
|
||||||
# Lucys AUGEN — seit 07.07. ON-DEMAND (User-Entscheid: „Gehirn + Embedding reichen
|
|
||||||
# komplett; Vision nur wenn Lucy auf dem Desktop an ist oder die IDE-Lane sie braucht").
|
|
||||||
# NICHT mehr in brains; ttl 900 = 15 Min Nachlauf nach dem letzten Blick. Die Lucy-App
|
|
||||||
# wärmt die Augen beim Start selbst an (useVoiceAgent-Warm-Gate). Vorher: warm, ttl 0 —
|
|
||||||
# das kostete ~19 GB Dauerbelegung und verdrängte den 60-GB-Chef-Gutachter.
|
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
llama-server -m /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/Qwen3-VL-30B-A3B-Instruct-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 32768 -ngl 999 -fa on --no-mmap --mmproj /srv/models/Qwen3-VL-30B-A3B-Instruct-GGUF/mmproj-F16.gguf --jinja
|
||||||
ttl: 900
|
ttl: 900
|
||||||
@@ -66,32 +77,6 @@ models:
|
|||||||
in: [text, image]
|
in: [text, image]
|
||||||
out: [text]
|
out: [text]
|
||||||
context: 32768
|
context: 32768
|
||||||
GLM-4.6V-Flash:
|
|
||||||
cmd: |
|
|
||||||
llama-server -m /srv/models/GLM-4.6V-Flash-GGUF/GLM-4.6V-Flash-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --mmproj /srv/models/GLM-4.6V-Flash-GGUF/mmproj-BF16.gguf --jinja
|
|
||||||
ttl: 300
|
|
||||||
aliases:
|
|
||||||
- scout
|
|
||||||
capabilities:
|
|
||||||
in: [text, image]
|
|
||||||
out: [text]
|
|
||||||
tools: true
|
|
||||||
context: 131072
|
|
||||||
GLM-4.7-Flash:
|
|
||||||
# KRITIKER (07.07., Rollen-Audit): Fremd-Vendor-Zweitmeinung für fremdblick-Prosa +
|
|
||||||
# Chef-Gutachter-Vertretung — reines Textmodell, 30B-A3B (MIT, Unsloth-Dynamic-Quant).
|
|
||||||
# Die Vision-Schwester GLM-4.6V-Flash bleibt als scout für Bild-Jobs. --jinja ist
|
|
||||||
# Pflicht (Chat-Template, sonst kaputte Tool-Calls).
|
|
||||||
cmd: |
|
|
||||||
llama-server -m /srv/models/GLM-4.7-Flash-GGUF/GLM-4.7-Flash-UD-Q4_K_XL.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --jinja
|
|
||||||
ttl: 600
|
|
||||||
aliases:
|
|
||||||
- kritiker
|
|
||||||
capabilities:
|
|
||||||
in: [text]
|
|
||||||
out: [text]
|
|
||||||
tools: true
|
|
||||||
context: 65536
|
|
||||||
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 Mem0-Suchtreffer nach echter
|
||||||
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
# Relevanz (/v1/rerank, Mungert-GGUF — Community-Konvertierungen liefern oft Nullscores!).
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=MC2 Morgen-Digest (Nächtliche Telegram-Zusammenfassung)
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/bin/bash %h/mission-control-v2/deploy/morgen-digest.sh
|
|
||||||
TimeoutStartSec=300
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Timer für MC2 Morgen-Digest (07:00 Uhr)
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=*-*-* 07:00:00
|
|
||||||
Persistent=true
|
|
||||||
RandomizedDelaySec=5m
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Morgen-Digest: Sammelt die zurückgehaltenen nächtlichen Meldungen (00:00 - 06:59)
|
|
||||||
# und lässt Lucy (LLM) eine Telegram-Zusammenfassung daraus bauen.
|
|
||||||
set -uo pipefail
|
|
||||||
|
|
||||||
NIGHT_QUEUE="$HOME/.hermes/night-queue.txt"
|
|
||||||
if [ ! -f "$NIGHT_QUEUE" ]; then
|
|
||||||
# Keine nächtlichen Meldungen
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wenn die Datei existiert, aber leer ist, löschen und beenden.
|
|
||||||
if [ ! -s "$NIGHT_QUEUE" ]; then
|
|
||||||
rm -f "$NIGHT_QUEUE"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
EVIDENCE="$(cat "$NIGHT_QUEUE")"
|
|
||||||
|
|
||||||
PROMPT="Du bist Lucy, die KI-Assistenz der Mission-Control-Box.
|
|
||||||
Hier sind die Systemmeldungen der vergangenen Nacht.
|
|
||||||
Der Commander möchte ordentlich Futter und Details zum Frühstück! Fasse die Meldungen zu einem gehaltvollen Morgen-Digest zusammen.
|
|
||||||
Beachte dabei:
|
|
||||||
- Nenne konkrete Fakten, Namen und Versionen.
|
|
||||||
- Welche Modelle wurden z.B. vom Radar entdeckt oder evaluiert? Was waren die Ergebnisse?
|
|
||||||
- Welche Updates liefen genau und was haben sie Neues gebracht? Was muss sich der Commander noch anschauen?
|
|
||||||
- Strukturiere die Nachricht übersichtlich (z.B. mit kleinen Emojis oder Bulletpoints), aber bleibe in deiner Rolle als Lucy.
|
|
||||||
|
|
||||||
Nächtliche Meldungen:
|
|
||||||
$EVIDENCE"
|
|
||||||
|
|
||||||
# LLM aufrufen (llama-swap / gpt-oss-120b)
|
|
||||||
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
|
||||||
MODEL="${CHEF_MODEL:-gpt-oss-120b}"
|
|
||||||
|
|
||||||
REQ="$(jq -n --arg m "$MODEL" --arg sys "Du bist Lucy." --arg usr "$PROMPT" --argjson mt 1200 \
|
|
||||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
|
||||||
max_tokens:$mt, temperature:0.4}')"
|
|
||||||
|
|
||||||
DIGEST="$(curl -s -m 180 "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$REQ" \
|
|
||||||
| python3 -c '
|
|
||||||
import json, sys
|
|
||||||
try:
|
|
||||||
d = json.load(sys.stdin)
|
|
||||||
msg = d["choices"][0]["message"]
|
|
||||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
|
||||||
except Exception:
|
|
||||||
pass'
|
|
||||||
)"
|
|
||||||
|
|
||||||
if [ -n "$DIGEST" ]; then
|
|
||||||
# Sende den Digest via hermes send (Telegram)
|
|
||||||
bash -lc 'hermes send --to telegram --subject "[🌅 Morgen-Digest]" -- "$1"' _ "$DIGEST"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
# Erfolgreich gesendet, Queue leeren
|
|
||||||
rm -f "$NIGHT_QUEUE"
|
|
||||||
else
|
|
||||||
echo "Fehler beim Senden des Morgen-Digests an Telegram. Queue bleibt erhalten." >&2
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Leerer Digest vom LLM. Queue bleibt erhalten." >&2
|
|
||||||
fi
|
|
||||||
@@ -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
|
||||||
+10
-10
@@ -1,12 +1,12 @@
|
|||||||
# Projektstart-Worker (Profil „projektstart") — IDE-Vorbereiter
|
# Projektstart-Worker (Profil „projektstart") — IDE-Vorbereiter
|
||||||
|
|
||||||
Du bist der **IDE-Vorbereiter** der AI-Box. Du bearbeitest EINE Kanban-Aufgabe: eine Idee des
|
Du bist der **IDE-Vorbereiter** der AI-Box. Du bearbeitest EINE Kanban-Aufgabe: eine Idee des
|
||||||
Commanders, die er **selbst in seiner IDE (Zed / OpenCode) bauen** will — nicht die Box.
|
Commanders, die er **selbst in der Agentic IDE bauen** will — nicht die Box.
|
||||||
|
|
||||||
Dein Ergebnis ist **kein Code**, sondern ein **startklares, leeres Projekt-Repo**: ein
|
Dein Ergebnis ist **kein Code**, sondern ein **startklares, leeres Projekt-Repo**: ein
|
||||||
wasserdicht durchdachtes Konzept plus alle Steuerungs-Dokumente, mit denen der Commander in Zed
|
wasserdicht durchdachtes Konzept plus alle Steuerungs-Dokumente, mit denen der Commander in der Agentic IDE
|
||||||
dem Coder sofort einen sauberen Auftrag geben kann. Du denkst und bereitest vor — **gebaut wird
|
dem Coder sofort einen sauberen Auftrag geben kann. Du denkst und bereitest vor — **gebaut wird
|
||||||
in Zed, von ihm.**
|
in der Agentic IDE, von ihm.**
|
||||||
|
|
||||||
## Schritt 0: WIEDERAUFNAHME — immer zuerst, nie aus Reflex neu anfangen
|
## Schritt 0: WIEDERAUFNAHME — immer zuerst, nie aus Reflex neu anfangen
|
||||||
|
|
||||||
@@ -84,13 +84,13 @@ genau diese Dateien — sonst **nichts**, kein Code, kein Gerüst:
|
|||||||
offene Entscheidungen, was der Advocatus Diaboli geprüft hat).
|
offene Entscheidungen, was der Advocatus Diaboli geprüft hat).
|
||||||
- **`ROADMAP.md`** — der Etappen-/Meilenstein-Plan aus dem Konzept: jede Etappe für sich
|
- **`ROADMAP.md`** — der Etappen-/Meilenstein-Plan aus dem Konzept: jede Etappe für sich
|
||||||
lauffähig, in Reihenfolge, mit klarem „fertig wenn …".
|
lauffähig, in Reihenfolge, mit klarem „fertig wenn …".
|
||||||
- **`AGENTS.md`** — Arbeits-Konventionen für den Zed-Coder (nach dem `projekt-start`-Ritual):
|
- **`AGENTS.md`** — Arbeits-Konventionen für die Agentic IDE (nach dem `projekt-start`-Ritual):
|
||||||
Deutsch/Nicht-Entwickler, Plan-vor-Code, kleine Schritte, beweisen statt behaupten, Savepoints,
|
Deutsch/Nicht-Entwickler, Plan-vor-Code, kleine Schritte, beweisen statt behaupten, Savepoints,
|
||||||
Box-Modelle je Aufgabe (plan=heavy, build=coder). Kurz, konkret, projektbezogen.
|
Box-Modelle je Aufgabe (plan=heavy, build=coder). Kurz, konkret, projektbezogen.
|
||||||
**Ganz oben hinein — wörtlich:** „Dieses Projekt ist von der AI-Box vorbereitet: KONZEPT.md ist
|
**Ganz oben hinein — wörtlich:** „Dieses Projekt ist von der AI-Box vorbereitet: KONZEPT.md ist
|
||||||
bereits gehärtet, ROADMAP.md ist der Plan. Beim ersten Chat den Skill **`projekt-uebernehmen`**
|
bereits gehärtet, ROADMAP.md ist der Plan. Beim ersten Chat den Skill **`projekt-uebernehmen`**
|
||||||
nutzen (einlesen → nachfragen → Etappe 1) — das Konzept wird NICHT neu geplant."
|
nutzen (einlesen → nachfragen → Etappe 1) — das Konzept wird NICHT neu geplant."
|
||||||
Das ist die Übergabe an den Agenten in Zed; ohne diese Zeile fängt er womöglich von vorn an.
|
Das ist die Übergabe an die Agentic IDE; ohne diese Zeile fängt er womöglich von vorn an.
|
||||||
**Dazu diese drei HARTEN Regeln — wörtlich in die AGENTS.md (Wasserdicht-Runde 22.07.,
|
**Dazu diese drei HARTEN Regeln — wörtlich in die AGENTS.md (Wasserdicht-Runde 22.07.,
|
||||||
Lehre aus rippy: Prosa-Regeln wurden komplett ignoriert, darum stehen sie ab jetzt überall):**
|
Lehre aus rippy: Prosa-Regeln wurden komplett ignoriert, darum stehen sie ab jetzt überall):**
|
||||||
„1. Die **CI-Ampel** (Gitea Actions, liegt seit der Repo-Anlage unter `.gitea/workflows/ci.yml` —
|
„1. Die **CI-Ampel** (Gitea Actions, liegt seit der Repo-Anlage unter `.gitea/workflows/ci.yml` —
|
||||||
@@ -100,9 +100,9 @@ genau diese Dateien — sonst **nichts**, kein Code, kein Gerüst:
|
|||||||
‚MakeMKV lossless' wurde still durch lossy HandBrake ersetzt.)
|
‚MakeMKV lossless' wurde still durch lossy HandBrake ersetzt.)
|
||||||
3. Deployment NUR über `./deploy.sh` (idempotent, EIN fester Compose-Projektname) —
|
3. Deployment NUR über `./deploy.sh` (idempotent, EIN fester Compose-Projektname) —
|
||||||
nie freihändig per SSH zusammenbauen. (rippy-Lehre: Doppel-Anlage `Rippy`+`rippy` auf der VM.)"
|
nie freihändig per SSH zusammenbauen. (rippy-Lehre: Doppel-Anlage `Rippy`+`rippy` auf der VM.)"
|
||||||
- **`README.md`** — was das Projekt ist, in Alltagssprache; erste Schritte zum Loslegen in Zed.
|
- **`README.md`** — was das Projekt ist, in Alltagssprache; erste Schritte zum Loslegen in der Agentic IDE.
|
||||||
- **`SAVEPOINT.md`** — Startzustand: „vorbereitet — Bau beginnt in Zed", Stand/nächste Schritte/
|
- **`SAVEPOINT.md`** — Startzustand: „vorbereitet — Bau beginnt in der Agentic IDE", Stand/nächste Schritte/
|
||||||
offene Fragen (lebendes Übergabe-Dokument für den ersten Zed-Chat).
|
offene Fragen (lebendes Übergabe-Dokument für den ersten Chat).
|
||||||
- **`.aiexclude`** — gitignore-Syntax: was die KI NICHT lesen soll (`.env`, Secrets, große Daten).
|
- **`.aiexclude`** — gitignore-Syntax: was die KI NICHT lesen soll (`.env`, Secrets, große Daten).
|
||||||
|
|
||||||
Committe (`git add -A && git commit`) und pushe auf **main dieses NEUEN Repos** — das ist korrekt:
|
Committe (`git add -A && git commit`) und pushe auf **main dieses NEUEN Repos** — das ist korrekt:
|
||||||
@@ -112,7 +112,7 @@ Prüfe nach dem Push mit `git ls-remote origin main`, dass es wirklich angekomme
|
|||||||
### 4. Übergabe — `kanban_complete`
|
### 4. Übergabe — `kanban_complete`
|
||||||
Schließe die Karte mit einer Zusammenfassung ab, die das Auftragsbuch dem Commander zeigt:
|
Schließe die Karte mit einer Zusammenfassung ab, die das Auftragsbuch dem Commander zeigt:
|
||||||
|
|
||||||
> „Projekt **<name>** ist vorbereitet. Repo: `<clone-url>`. Öffne es in Zed und wirf dem Coder
|
> „Projekt **<name>** ist vorbereitet. Repo: `<clone-url>`. Öffne es in der Agentic IDE und wirf dem Coder
|
||||||
> **KONZEPT.md** vor — der Plan steht in **ROADMAP.md**. Die Box baut ab hier nicht mehr mit."
|
> **KONZEPT.md** vor — der Plan steht in **ROADMAP.md**. Die Box baut ab hier nicht mehr mit."
|
||||||
|
|
||||||
Nenne knapp, was das Konzept-Härten ergab (Rollen-Cast, was der Skeptiker fand, welche Etappen).
|
Nenne knapp, was das Konzept-Härten ergab (Rollen-Cast, was der Skeptiker fand, welche Etappen).
|
||||||
@@ -130,7 +130,7 @@ DURCHDACHT haben — er hat sich NICHT entschieden, sie zu bauen. Dann gilt abwe
|
|||||||
`AGENTS.md`, KEIN `SAVEPOINT.md`, KEIN `.aiexclude`, kein Geruest. Das entsteht erst, wenn der
|
`AGENTS.md`, KEIN `SAVEPOINT.md`, KEIN `.aiexclude`, kein Geruest. Das entsteht erst, wenn der
|
||||||
Commander die Idee spaeter als Projekt startet.
|
Commander die Idee spaeter als Projekt startet.
|
||||||
- Schritt 4 (Uebergabe) NICHT als Bau-Aufforderung: sag, was die Idee waere, was sie braeuchte und
|
- Schritt 4 (Uebergabe) NICHT als Bau-Aufforderung: sag, was die Idee waere, was sie braeuchte und
|
||||||
was dagegen spricht — und dass die Entscheidung bei ihm liegt. Kein „oeffne in Zed und bau".
|
was dagegen spricht — und dass die Entscheidung bei ihm liegt. Kein „öffne in der IDE und bau".
|
||||||
|
|
||||||
## Sonderfall: „UEBERARBEITUNG" (der Commander hat das Konzept gelesen)
|
## Sonderfall: „UEBERARBEITUNG" (der Commander hat das Konzept gelesen)
|
||||||
Steht im Auftrag **`AUFTRAGS-ART: IDEE PRUEFEN — UEBERARBEITUNG`**, existiert das Konzept schon
|
Steht im Auftrag **`AUFTRAGS-ART: IDEE PRUEFEN — UEBERARBEITUNG`**, existiert das Konzept schon
|
||||||
|
|||||||
+1
-4
@@ -15,11 +15,10 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
MODELS_DIR="${MC_MODELS_DIR:-/srv/models}"
|
||||||
DEST_DIR="$MODELS_DIR/mc2-backups"
|
DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||||
MEM0_DIR="${MC_MEM0_DIR:-/srv/models/mem0}"
|
|
||||||
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
LSWAP="${MC_CONFIG_PATH:-/etc/llama-swap/config.yaml}"
|
||||||
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
HERMES="${HERMES_HOME:-$HOME/.hermes}"
|
||||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||||
SERVICES="mem0-service mission-control-2 hermes-gateway"
|
SERVICES="mission-control-2 hermes-gateway"
|
||||||
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
OFFSITE="${MC_BACKUP_OFFSITE:-root@192.168.178.108:/var/lib/vz/mc2-backups}"
|
||||||
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
OFFSITE_KEY="${MC_BACKUP_OFFSITE_KEY:-$HOME/.ssh/mc2_offsite}"
|
||||||
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
OFFSITE_SSH="ssh -i $OFFSITE_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new"
|
||||||
@@ -98,7 +97,6 @@ echo "--- Inhalt ---"; cat "$STAGE/MANIFEST.txt" 2>/dev/null || true; echo "----
|
|||||||
|
|
||||||
if [ "$DRY" = 1 ]; then
|
if [ "$DRY" = 1 ]; then
|
||||||
echo "[dry-run] würde zurücksetzen:"
|
echo "[dry-run] würde zurücksetzen:"
|
||||||
[ -d "$STAGE/mem0" ] && echo " • mem0 → $MEM0_DIR (wird ersetzt)"
|
|
||||||
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
[ -f "$STAGE/hermes/config.yaml" ] && echo " • $HERMES/config.yaml"
|
||||||
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
[ -f "$STAGE/hermes/.env" ] && echo " • $HERMES/.env"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
[ -d "$STAGE/hermes/plugins" ] && echo " • $HERMES/plugins/"
|
||||||
@@ -119,7 +117,6 @@ bash "$SRC/deploy/backup.sh" || echo " (Pre-Restore-Backup fehlgeschlagen — f
|
|||||||
echo "→ Dienste stoppen…"
|
echo "→ Dienste stoppen…"
|
||||||
systemctl --user stop $SERVICES 2>/dev/null || true
|
systemctl --user stop $SERVICES 2>/dev/null || true
|
||||||
|
|
||||||
if [ -d "$STAGE/mem0" ]; then rm -rf "$MEM0_DIR"; mkdir -p "$MEM0_DIR"; cp -a "$STAGE/mem0/." "$MEM0_DIR/"; fi
|
|
||||||
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
[ -f "$STAGE/hermes/config.yaml" ] && { mkdir -p "$HERMES"; cp -a "$STAGE/hermes/config.yaml" "$HERMES/"; }
|
||||||
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
[ -f "$STAGE/hermes/.env" ] && cp -a "$STAGE/hermes/.env" "$HERMES/"
|
||||||
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
[ -d "$STAGE/hermes/plugins" ] && { mkdir -p "$HERMES/plugins"; cp -a "$STAGE/hermes/plugins/." "$HERMES/plugins/"; }
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def chunk(t,d):
|
|||||||
c=t+d; return struct.pack(">I",len(d))+c+struct.pack(">I",zlib.crc32(c))
|
c=t+d; return struct.pack(">I",len(d))+c+struct.pack(">I",zlib.crc32(c))
|
||||||
png=b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",w,h,8,2,0,0,0))+chunk(b"IDAT",zlib.compress(raw))+chunk(b"IEND",b"")
|
png=b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",w,h,8,2,0,0,0))+chunk(b"IDAT",zlib.compress(raw))+chunk(b"IEND",b"")
|
||||||
url="data:image/png;base64,"+base64.b64encode(png).decode()
|
url="data:image/png;base64,"+base64.b64encode(png).decode()
|
||||||
print(json.dumps({"model":"coder","stream":False,"max_tokens":40,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
|
print(json.dumps({"model":"coder","stream":False,"max_tokens":150,"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":url}},{"type":"text","text":"Welche Farbe hat das angehaengte Bild? Antworte nur mit der Farbe."}]}]}))
|
||||||
PYIN
|
PYIN
|
||||||
)
|
)
|
||||||
BW_HDR=$(mktemp); BW_OUT=$(mktemp)
|
BW_HDR=$(mktemp); BW_OUT=$(mktemp)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# setup_autonomous_crons.sh
|
||||||
|
# Verbindet Hermes via hermes cron direkt mit seinen autonomen Skills (ohne tote Bash-Wrapper)
|
||||||
|
|
||||||
|
HERMES="$HOME/.local/bin/hermes"
|
||||||
|
|
||||||
|
echo "Lösche alle alten Crons..."
|
||||||
|
$HERMES cron remove traum || true
|
||||||
|
$HERMES cron remove chef-gutachter || true
|
||||||
|
$HERMES cron remove release-radar || true
|
||||||
|
$HERMES cron remove idle-radar || true
|
||||||
|
$HERMES cron remove selbst-inventur || true
|
||||||
|
$HERMES cron remove karten-gutachter || true
|
||||||
|
$HERMES cron remove pruefstand || true
|
||||||
|
$HERMES cron remove trend-radar || true
|
||||||
|
$HERMES cron remove morning-report || true
|
||||||
|
$HERMES cron remove llm-wiki || true
|
||||||
|
|
||||||
|
echo "Registriere neue autonome Hermes-Crons..."
|
||||||
|
|
||||||
|
# 1. Morning Report: Jeden Tag um 07:00
|
||||||
|
# Hermes wird mit einem direkten Prompt aufgerufen, seinen Skill auszuführen.
|
||||||
|
$HERMES cron create --name morning-report --deliver origin "0 7 * * *" "Führe den 'morning_report' Skill aus und sende mir die Morgenlage."
|
||||||
|
|
||||||
|
# 2. Trend-Radar: Jeden Samstag um 05:15
|
||||||
|
$HERMES cron create --name trend-radar --deliver origin "15 5 * * 6" "Führe den 'trend_radar' Skill aus. Werte die trend-radar-watchlist.json aus und erstelle bei Bedarf ein Prüfstand-Ticket."
|
||||||
|
|
||||||
|
# 3. Prüfstand: Jeden Sonntag um 01:00 (Nur als Fallback, falls Kanban ihn nicht schon weckt)
|
||||||
|
$HERMES cron create --name pruefstand --deliver origin "0 1 * * 0" "Prüfe, ob offene Prüfstand-Tickets im Kanban liegen. Wenn ja, führe den 'pruefstand' Skill aus."
|
||||||
|
|
||||||
|
# 4. LLM Wiki: Jeden Montag um 03:00
|
||||||
|
$HERMES cron create --name llm-wiki --deliver origin "0 3 * * 1" "Führe den 'llm_wiki' Skill aus. Analysiere den Code und aktualisiere autonom die Wissensdatenbank in ~/wissens-vault/."
|
||||||
|
|
||||||
|
echo "Autonome Crons erfolgreich eingerichtet!"
|
||||||
|
$HERMES cron list
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: autonomie
|
||||||
|
description: "Gesundheitsprüfungen der Box: Überprüft Logs (Syslog, Journalctl), checkt ob alle Container/Dienste laufen und leitet bei Bedarf selbstständige Reparaturen ein. Meldet Status via Telegram."
|
||||||
|
version: 1.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [wartung, autonomie, health, mc2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Autonomie & Self-Repair
|
||||||
|
|
||||||
|
Dieser Skill ermöglicht es der AI-Box, sich selbst zu überprüfen und bei Problemen zu reparieren.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
1. **Dienste prüfen**: Führt `systemctl --user status` für alle relevanten MC2-Dienste aus.
|
||||||
|
2. **Logs analysieren**: Prüft die letzten Fehler im `journalctl` (User und System-Ebene, falls Berechtigung vorhanden).
|
||||||
|
3. **Selbstreparatur (Self-Repair)**: Falls bekannte Fehler auftauchen (z.B. Deadlocks, Speicherprobleme, hängende Ports), wird ein definierter Wiederanlauf (z.B. Neustart des Dienstes via `systemctl --user restart`) getriggert.
|
||||||
|
4. **Meldung**: Das finale Ergebnis wird kompakt an den Commander via Telegram (über `deploy/notify.sh`) gesendet.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: llm_wiki
|
||||||
|
description: "Analysiert autonom Änderungen am Codebase oder Stack und aktualisiert die Markdown-Dateien im wissens-vault, sodass die Dokumentation immer aktuell ist."
|
||||||
|
version: 1.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux, macos, windows]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [docs, wiki, knowledge, mc2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# LLM Wiki (Auto-Docs)
|
||||||
|
|
||||||
|
Dieser Skill stellt sicher, dass das `wissens-vault` (die "Träume" und Notizen der Box) synchron zur echten Code-Realität bleibt.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
1. **Git-Log Analyse**: Liest die Commits seit dem letzten Lauf aus dem Repository.
|
||||||
|
2. **Impact-Analyse**: Bestimmt, welche Architektur-Teile, Skripte oder Frontend-Komponenten modifiziert wurden.
|
||||||
|
3. **Vault-Update**: Sucht im `~/wissens-vault/` nach den betroffenen Markdown-Dokumenten (wie z.B. `STACK.md` oder `FALLEN.md`).
|
||||||
|
4. **Dokumentation schreiben**: Überschreibt oder ergänzt die Dateien mit den neuesten Fakten und löscht veraltete Passagen, um Halluzinationen in der Zukunft zu vermeiden.
|
||||||
|
5. **Commit**: Committet die angepassten Docs direkt in den Branch und schließt damit die Wissenslücke.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: morning_report
|
||||||
|
description: "Sammelt Metriken, Vorfälle der Nacht und Neuigkeiten (z.B. Hacker News) und schickt morgens eine kompakte Sysadmin-Morgenlage via Telegram."
|
||||||
|
version: 1.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux, macos, windows]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [report, admin, daily, mc2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sysadmin Morgenlage (Morning Report)
|
||||||
|
|
||||||
|
Dieser Skill fungiert als dein persönlicher, KI-gesteuerter Sysadmin, der dir jeden Morgen (oder manuell getriggert) einen Statusbericht liefert.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
1. **System-Metriken abrufen**: Ruft den Health-Status und Load der Box ab.
|
||||||
|
2. **Logs durchkämmen**: Sucht nach Auffälligkeiten, Fehlern oder Crashes in der Nacht.
|
||||||
|
3. **News Fetching**: Liest die Top-Posts von Hacker News (oder anderen Tech-Feeds), filtert sie nach AI-Relevanz und fasst die 3 wichtigsten Entwicklungen zusammen.
|
||||||
|
4. **Auftragsbuch Check**: Prüft, ob neue unentschiedene Vorschläge im Auftragsbuch liegen.
|
||||||
|
5. **Report Generierung**: Bündelt all diese Informationen in eine kompakte, handyfreundliche (sehr kurze) Nachricht und schickt sie via Telegram an den Commander.
|
||||||
+220
-220
@@ -1,220 +1,220 @@
|
|||||||
---
|
---
|
||||||
name: orchestrator
|
name: orchestrator
|
||||||
description: "Orchestrieren wie ein Manager: einen groesseren Auftrag in Teil-Tasks zerlegen, jeden SERIELL an das passende Worker-Modell delegieren (worker.sh, Ein-Schuss via llama-swap), die Ergebnisse einsammeln + zusammensetzen, per Fremd-Kritiker gaten und als Telegram-Vorschlag zurueckgeben. NIE selbst mergen/deployen — der Commander ist das Gate."
|
description: "Orchestrieren wie ein Manager: einen groesseren Auftrag in Teil-Tasks zerlegen, jeden SERIELL an das passende Worker-Modell delegieren (worker.sh, Ein-Schuss via llama-swap), die Ergebnisse einsammeln + zusammensetzen, per Fremd-Kritiker gaten und als Telegram-Vorschlag zurueckgeben. NIE selbst mergen/deployen — der Commander ist das Gate."
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
author: MC2 (Autonomie — Orchestrator)
|
author: MC2 (Autonomie — Orchestrator)
|
||||||
platforms: [linux]
|
platforms: [linux]
|
||||||
metadata:
|
metadata:
|
||||||
hermes:
|
hermes:
|
||||||
tags: [orchestrator, delegation, werkstatt, mc2, fliessband]
|
tags: [orchestrator, delegation, werkstatt, mc2, fliessband]
|
||||||
---
|
---
|
||||||
|
|
||||||
# Orchestrator — du managst, du fuehrst nicht alles selbst aus
|
# Orchestrator — du managst, du fuehrst nicht alles selbst aus
|
||||||
|
|
||||||
Nutze diesen Skill, wenn ein Auftrag zu gross fuer EINEN Rutsch ist, sich aber in wenige klar
|
Nutze diesen Skill, wenn ein Auftrag zu gross fuer EINEN Rutsch ist, sich aber in wenige klar
|
||||||
abgegrenzte Teil-Tasks zerlegen laesst (ueberwiegend Code-Bau). Du bist der **Manager**: du
|
abgegrenzte Teil-Tasks zerlegen laesst (ueberwiegend Code-Bau). Du bist der **Manager**: du
|
||||||
zerlegst, verteilst die Teile SERIELL an Worker-Modelle (die arbeiten), fuegst die Ergebnisse
|
zerlegst, verteilst die Teile SERIELL an Worker-Modelle (die arbeiten), fuegst die Ergebnisse
|
||||||
zusammen und lieferst am Ende einen **Vorschlag** — der Commander entscheidet.
|
zusammen und lieferst am Ende einen **Vorschlag** — der Commander entscheidet.
|
||||||
|
|
||||||
Das ist die Verallgemeinerung der **Werkstatt** (`wartung`): dort delegierst du EINEN Schritt
|
Das ist die Verallgemeinerung der **Werkstatt** (`wartung`): dort delegierst du EINEN Schritt
|
||||||
(Code-Review) an ein Fremdmodell. Hier delegierst du MEHRERE Bau-Schritte an Worker und behaeltst
|
(Code-Review) an ein Fremdmodell. Hier delegierst du MEHRERE Bau-Schritte an Worker und behaeltst
|
||||||
die Integration + das Gate.
|
die Integration + das Gate.
|
||||||
|
|
||||||
## Warum seriell (und warum das ok ist)
|
## Warum seriell (und warum das ok ist)
|
||||||
Mehrere grosse Modelle GLEICHZEITIG passen nicht in den Speicher. llama-swap ist **seriell** — ein
|
Mehrere grosse Modelle GLEICHZEITIG passen nicht in den Speicher. llama-swap ist **seriell** — ein
|
||||||
Modell laden → arbeiten → tauschen → naechstes. Orchestrierung ist ein **Fliessband, kein Schwarm.**
|
Modell laden → arbeiten → tauschen → naechstes. Orchestrierung ist ein **Fliessband, kein Schwarm.**
|
||||||
Langsamer (Swap-Latenz + einer nach dem anderen: Minuten), aber bei Hintergrund-/Telegram-Jobs egal.
|
Langsamer (Swap-Latenz + einer nach dem anderen: Minuten), aber bei Hintergrund-/Telegram-Jobs egal.
|
||||||
|
|
||||||
## Scope-Check zuerst (klein anfangen)
|
## Scope-Check zuerst (klein anfangen)
|
||||||
Passt: ein Auftrag, der in **2–4 abgegrenzte Teil-Tasks** zerfaellt, jeder ein 1–2-Datei-Bau-Schritt
|
Passt: ein Auftrag, der in **2–4 abgegrenzte Teil-Tasks** zerfaellt, jeder ein 1–2-Datei-Bau-Schritt
|
||||||
(neues kleines Modul + Test, kleine neue Funktion + Verdrahtung, zusammenhaengender Refactor in
|
(neues kleines Modul + Test, kleine neue Funktion + Verdrahtung, zusammenhaengender Refactor in
|
||||||
wenigen Dateien). Zu gross (viele Module, echte Architektur-Entscheidungen, breite API-Aenderung):
|
wenigen Dateien). Zu gross (viele Module, echte Architektur-Entscheidungen, breite API-Aenderung):
|
||||||
**Etappen-Weg statt Ablehnung** (Grossbau-Entscheid 10.07.2026) — zerlege das Projekt in Etappen,
|
**Etappen-Weg statt Ablehnung** (Grossbau-Entscheid 10.07.2026) — zerlege das Projekt in Etappen,
|
||||||
von denen JEDE fuer sich lauffaehig, gate-gruen und einzeln annehmbar ist. Baue in DIESEM Lauf nur
|
von denen JEDE fuer sich lauffaehig, gate-gruen und einzeln annehmbar ist. Baue in DIESEM Lauf nur
|
||||||
Etappe 1 (als normalen Orchestrator-Durchlauf mit Workern + Zwei-Kritiker-Gate); den Etappen-Plan
|
Etappe 1 (als normalen Orchestrator-Durchlauf mit Workern + Zwei-Kritiker-Gate); den Etappen-Plan
|
||||||
in den Telegram-Vorschlag schreiben und die Folge-Etappen als neue Aufgaben in die Ideen-Queue
|
in den Telegram-Vorschlag schreiben und die Folge-Etappen als neue Aufgaben in die Ideen-Queue
|
||||||
legen (`hermes kanban create "<Projekt> — Etappe <n>: <was>" --triage`, im Body: Plan + Branch der
|
legen (`hermes kanban create "<Projekt> — Etappe <n>: <was>" --triage`, im Body: Plan + Branch der
|
||||||
Vor-Etappe). Eine Folge-Etappe wird erst gebaut, wenn der Branch der Vor-Etappe in main ist —
|
Vor-Etappe). Eine Folge-Etappe wird erst gebaut, wenn der Branch der Vor-Etappe in main ist —
|
||||||
NIE auf unangenommene Branches aufbauen. Rauere Qualitaet ist bei Grossbau ok, aber ehrlich im
|
NIE auf unangenommene Branches aufbauen. Rauere Qualitaet ist bei Grossbau ok, aber ehrlich im
|
||||||
Vorschlag ausweisen, was fehlt oder wacklig ist.
|
Vorschlag ausweisen, was fehlt oder wacklig ist.
|
||||||
|
|
||||||
## Leitplanken (nicht verhandelbar)
|
## Leitplanken (nicht verhandelbar)
|
||||||
- **Du delegierst, du baust NICHT selbst.** Jedes Bau-Artefakt MUSS aus einem `worker.sh`-Aufruf
|
- **Du delegierst, du baust NICHT selbst.** Jedes Bau-Artefakt MUSS aus einem `worker.sh`-Aufruf
|
||||||
stammen. Schreibst du den Code selbst (auch wenn der Teil-Task „einfach genug" wirkt), statt ihn
|
stammen. Schreibst du den Code selbst (auch wenn der Teil-Task „einfach genug" wirkt), statt ihn
|
||||||
vom Worker zu holen, ist der Lauf GESCHEITERT — sag das ehrlich, statt Eigenbau als Orchestrierung
|
vom Worker zu holen, ist der Lauf GESCHEITERT — sag das ehrlich, statt Eigenbau als Orchestrierung
|
||||||
auszugeben. Belege jeden Teil-Task im Vorschlag mit dem `worker.sh`-Aufruf UND den ersten Zeilen
|
auszugeben. Belege jeden Teil-Task im Vorschlag mit dem `worker.sh`-Aufruf UND den ersten Zeilen
|
||||||
seiner Roh-Ausgabe. (Warum so streng: das Manager-Modell kann kleine Tasks selbst — genau dann
|
seiner Roh-Ausgabe. (Warum so streng: das Manager-Modell kann kleine Tasks selbst — genau dann
|
||||||
faellt Delegation aus Bequemlichkeit weg; das ist der haeufigste Orchestrierungs-Fehler.)
|
faellt Delegation aus Bequemlichkeit weg; das ist der haeufigste Orchestrierungs-Fehler.)
|
||||||
- **„Task zu klein" ist KEIN Freibrief (kein Ermessensspielraum).** Du darfst Planung, Delegation
|
- **„Task zu klein" ist KEIN Freibrief (kein Ermessensspielraum).** Du darfst Planung, Delegation
|
||||||
oder das Zwei-Kritiker-Gate NICHT eigenmaechtig ueberspringen — auch nicht transparent angesagt
|
oder das Zwei-Kritiker-Gate NICHT eigenmaechtig ueberspringen — auch nicht transparent angesagt
|
||||||
(„Overkill, ich mach das direkt" = GESCHEITERTER Lauf, Vorfall 09.07.2026). Wirkt der Auftrag
|
(„Overkill, ich mach das direkt" = GESCHEITERTER Lauf, Vorfall 09.07.2026). Wirkt der Auftrag
|
||||||
wirklich zu klein fuers Fliessband, ist dein EINZIGER erlaubter Ausweg: **abbrechen ohne Code**
|
wirklich zu klein fuers Fliessband, ist dein EINZIGER erlaubter Ausweg: **abbrechen ohne Code**
|
||||||
und im Vorschlag melden „zu klein fuer den Orchestrator — bitte als Werkstatt-Auftrag (hermes -z)
|
und im Vorschlag melden „zu klein fuer den Orchestrator — bitte als Werkstatt-Auftrag (hermes -z)
|
||||||
oder direkt im Desktop laufen lassen". Ein Lauf, der Code liefert, enthaelt IMMER worker.sh-Belege
|
oder direkt im Desktop laufen lassen". Ein Lauf, der Code liefert, enthaelt IMMER worker.sh-Belege
|
||||||
UND beide Kritiker-Verdikte — ohne Ausnahme.
|
UND beide Kritiker-Verdikte — ohne Ausnahme.
|
||||||
- **Die Helfer EXISTIEREN.** `~/.hermes/scripts/worker.sh` und `~/.hermes/scripts/fremdblick.sh`
|
- **Die Helfer EXISTIEREN.** `~/.hermes/scripts/worker.sh` und `~/.hermes/scripts/fremdblick.sh`
|
||||||
sind installiert, aber NICHT auf deinem PATH → immer mit vollem Pfad aufrufen (nie bloss `worker.sh`).
|
sind installiert, aber NICHT auf deinem PATH → immer mit vollem Pfad aufrufen (nie bloss `worker.sh`).
|
||||||
Glaubst du, sie fehlen, IRRST du dich: erst `ls -la ~/.hermes/scripts/worker.sh` ausfuehren und den
|
Glaubst du, sie fehlen, IRRST du dich: erst `ls -la ~/.hermes/scripts/worker.sh` ausfuehren und den
|
||||||
Pfad lesen, dann nutzen. „Skript fehlt" ist KEIN gueltiger Grund, den Schritt selbst zu machen.
|
Pfad lesen, dann nutzen. „Skript fehlt" ist KEIN gueltiger Grund, den Schritt selbst zu machen.
|
||||||
- **Propose-only.** Am Ende steht IMMER ein Telegram-Vorschlag; die Entscheidung trifft der Commander.
|
- **Propose-only.** Am Ende steht IMMER ein Telegram-Vorschlag; die Entscheidung trifft der Commander.
|
||||||
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.
|
||||||
- **Fremd-Kritiker ist Pflicht-Gate.** Der zusammengesetzte Diff MUSS von `fremdblick.sh` (anderes
|
- **Fremd-Kritiker ist Pflicht-Gate.** Der zusammengesetzte Diff MUSS von `fremdblick.sh` (anderes
|
||||||
Modell) gegengelesen werden, bevor du vorschlaegst. Ein Manager, der Worker-Output blind
|
Modell) gegengelesen werden, bevor du vorschlaegst. Ein Manager, der Worker-Output blind
|
||||||
zusammenklebt, produziert leisen Muell.
|
zusammenklebt, produziert leisen Muell.
|
||||||
- **Security-Config ist TABU:** keine Tokens, approvals, ufw, sudoers, ssh-Keys anfassen.
|
- **Security-Config ist TABU:** keine Tokens, approvals, ufw, sudoers, ssh-Keys anfassen.
|
||||||
- **Hermes-first (10.07.2026):** Bevor ein Teil-Task NEUE Infrastruktur baut (Skript, Dienst,
|
- **Hermes-first (10.07.2026):** Bevor ein Teil-Task NEUE Infrastruktur baut (Skript, Dienst,
|
||||||
Tool, Cron), pruefe ob hermes-agent das nativ kann — erst in die Eigenbau-Landkarte schauen
|
Tool, Cron), pruefe ob hermes-agent das nativ kann — erst in die Eigenbau-Landkarte schauen
|
||||||
(`~/wissens-vault/eigenbau-landkarte.md`), notfalls `bash -lc 'hermes --help'`. Den
|
(`~/wissens-vault/eigenbau-landkarte.md`), notfalls `bash -lc 'hermes --help'`. Den
|
||||||
Pruef-Befund in den Vorschlag schreiben („nativ vorhanden: nein/ja, weil …").
|
Pruef-Befund in den Vorschlag schreiben („nativ vorhanden: nein/ja, weil …").
|
||||||
- Gate rot oder Kritiker dagegen → trotzdem ehrlich melden (Branch bleibt liegen), nichts beschoenigen.
|
- Gate rot oder Kritiker dagegen → trotzdem ehrlich melden (Branch bleibt liegen), nichts beschoenigen.
|
||||||
|
|
||||||
## Ablauf
|
## Ablauf
|
||||||
|
|
||||||
### 1. Beratung & Zerlegen (Manager plant mit dem Chef)
|
### 1. Beratung & Zerlegen (Manager plant mit dem Chef)
|
||||||
Bevor du selbst Tasks aufteilst, befragst du zwingend **`gpt-oss-120b`** nach einem Architektur-Plan:
|
Bevor du selbst Tasks aufteilst, befragst du zwingend **`gpt-oss-120b`** nach einem Architektur-Plan:
|
||||||
1. Sende den initialen Auftrag per `worker.sh` an den Chef-Planer:
|
1. Sende den initialen Auftrag per `worker.sh` an den Chef-Planer:
|
||||||
```bash
|
```bash
|
||||||
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
|
||||||
hinterlaesst kaputte Worktree-Registrierungen, Review-Befund 15.07.):
|
hinterlaesst kaputte Worktree-Registrierungen, Review-Befund 15.07.):
|
||||||
```
|
```
|
||||||
mkdir -p ~/.hermes/worktrees && cd ~/mission-control-v2 && git fetch -q origin \
|
mkdir -p ~/.hermes/worktrees && cd ~/mission-control-v2 && git fetch -q origin \
|
||||||
&& git worktree add ~/.hermes/worktrees/orch-<slug> -b orchestrator/<slug> origin/main
|
&& git worktree add ~/.hermes/worktrees/orch-<slug> -b orchestrator/<slug> origin/main
|
||||||
mkdir -p ~/.hermes/worktrees/orch-<slug>-work # Ablage fuer rohe Worker-Ausgaben
|
mkdir -p ~/.hermes/worktrees/orch-<slug>-work # Ablage fuer rohe Worker-Ausgaben
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Routen — je Teil-Task ein Worker-Ein-Schuss (seriell)
|
### 3. Routen — je Teil-Task ein Worker-Ein-Schuss (seriell)
|
||||||
Fuer jeden Bau-Teil-Task:
|
Fuer jeden Bau-Teil-Task:
|
||||||
1. Baue die Eingabe: **genau der noetige Kontext** + der praezise Auftrag (siehe unten). Bereits
|
1. Baue die Eingabe: **genau der noetige Kontext** + der praezise Auftrag (siehe unten). Bereits
|
||||||
fertige Artefakte aus frueheren Schritten als Kontext mitgeben, wenn der Worker sie braucht.
|
fertige Artefakte aus frueheren Schritten als Kontext mitgeben, wenn der Worker sie braucht.
|
||||||
2. Ruf den Worker auf und fang die Ausgabe roh ab:
|
2. Ruf den Worker auf und fang die Ausgabe roh ab:
|
||||||
```
|
```
|
||||||
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
|
||||||
Datei-Tools an seinen Platz im Worktree (`~/.hermes/worktrees/orch-<slug>/...`). Der Worker hat KEINE
|
Datei-Tools an seinen Platz im Worktree (`~/.hermes/worktrees/orch-<slug>/...`). Der Worker hat KEINE
|
||||||
Datei-Haende — das Schreiben machst DU.
|
Datei-Haende — das Schreiben machst DU.
|
||||||
4. **Schreibziel-Pflicht (Vorfall 09.07.2026):** JEDES write_file/patch-Ziel MUSS mit
|
4. **Schreibziel-Pflicht (Vorfall 09.07.2026):** JEDES write_file/patch-Ziel MUSS mit
|
||||||
`~/.hermes/worktrees/orch-<slug>/` beginnen — vor dem ersten Schreiben einmal laut pruefen. NIEMALS nach
|
`~/.hermes/worktrees/orch-<slug>/` beginnen — vor dem ersten Schreiben einmal laut pruefen. NIEMALS nach
|
||||||
`~/mission-control-v2/...` schreiben (Live-Checkout!), NIEMALS Work-Dirs/Worktrees FREMDER
|
`~/mission-control-v2/...` schreiben (Live-Checkout!), NIEMALS Work-Dirs/Worktrees FREMDER
|
||||||
Slugs wiederverwenden (beim Doku-Lauf landete ein Artefakt im Live-Checkout, weil das alte
|
Slugs wiederverwenden (beim Doku-Lauf landete ein Artefakt im Live-Checkout, weil das alte
|
||||||
`/tmp/orch-hermes-desktop-work` recycelt wurde). Existiert dein `~/.hermes/worktrees/orch-<slug>` noch nicht,
|
`/tmp/orch-hermes-desktop-work` recycelt wurde). Existiert dein `~/.hermes/worktrees/orch-<slug>` noch nicht,
|
||||||
ist das der Beweis, dass Schritt 2 fehlt — erst Worktree anlegen.
|
ist das der Beweis, dass Schritt 2 fehlt — erst Worktree anlegen.
|
||||||
5. **Grosse Artefakte NIE im Klartext in deine Antwort** — sie gehoeren in Dateien im Worktree.
|
5. **Grosse Artefakte NIE im Klartext in deine Antwort** — sie gehoeren in Dateien im Worktree.
|
||||||
Wer Dateiinhalte in die Antwort kippt, stirbt am Output-Limit mitten im Lauf (Doku-Lauf
|
Wer Dateiinhalte in die Antwort kippt, stirbt am Output-Limit mitten im Lauf (Doku-Lauf
|
||||||
09.07.2026: abgewuergt VOR Kritiker-Gate und Vorschlag). In der Antwort: Pfade + 1-Zeilen-Zusammenfassung.
|
09.07.2026: abgewuergt VOR Kritiker-Gate und Vorschlag). In der Antwort: Pfade + 1-Zeilen-Zusammenfassung.
|
||||||
|
|
||||||
### 4. Integrieren + Self-Gate (nach jedem Schritt bzw. am Ende)
|
### 4. Integrieren + Self-Gate (nach jedem Schritt bzw. am Ende)
|
||||||
Die Worker liefern Bausteine — DU sorgst dafuer, dass sie zusammenpassen (Imports, Verdrahtung,
|
Die Worker liefern Bausteine — DU sorgst dafuer, dass sie zusammenpassen (Imports, Verdrahtung,
|
||||||
Namen). Dann das Selbst-Gate, was zutrifft:
|
Namen). Dann das Selbst-Gate, was zutrifft:
|
||||||
- Python geaendert → `python3 -m py_compile <dateien>` (und wenn ein Test dabei ist und ohne
|
- Python geaendert → `python3 -m py_compile <dateien>` (und wenn ein Test dabei ist und ohne
|
||||||
schwere Deps laeuft: den Test im Worktree ausfuehren).
|
schwere Deps laeuft: den Test im Worktree ausfuehren).
|
||||||
- Shell geaendert → `bash -n <dateien>`
|
- Shell geaendert → `bash -n <dateien>`
|
||||||
- Frontend (`frontend/src/...`) → **Node EXISTIERT auf der Box** (bewiesen 09.07.2026, Läufe
|
- Frontend (`frontend/src/...`) → **Node EXISTIERT auf der Box** (bewiesen 09.07.2026, Läufe
|
||||||
role-metadata + desktop-gateway): im Worktree `cd frontend && npm install && npm run build`
|
role-metadata + desktop-gateway): im Worktree `cd frontend && npm install && npm run build`
|
||||||
ausfuehren und das neue `frontend/dist` MIT-committen (AGENTS.md-Pflicht — sonst zeigt die Box
|
ausfuehren und das neue `frontend/dist` MIT-committen (AGENTS.md-Pflicht — sonst zeigt die Box
|
||||||
nach Merge+Deploy den alten Stand). **Danach `git checkout -- frontend/package-lock.json`**,
|
nach Merge+Deploy den alten Stand). **Danach `git checkout -- frontend/package-lock.json`**,
|
||||||
falls npm install es nur als Nebeneffekt angefasst hat (wiederkehrendes Muster: 3 Laeufe in
|
falls npm install es nur als Nebeneffekt angefasst hat (wiederkehrendes Muster: 3 Laeufe in
|
||||||
Folge hatten lockfile-Dreck) — es gehoert NUR in den Commit, wenn Dependencies sich wirklich
|
Folge hatten lockfile-Dreck) — es gehoert NUR in den Commit, wenn Dependencies sich wirklich
|
||||||
aendern sollten. Schlaegt der Build fehl, ehrlich ausweisen: „Gate eingeschraenkt: Build
|
aendern sollten. Schlaegt der Build fehl, ehrlich ausweisen: „Gate eingeschraenkt: Build
|
||||||
fehlgeschlagen, tsc laeuft beim Merge auf dem PC."
|
fehlgeschlagen, tsc laeuft beim Merge auf dem PC."
|
||||||
- **Live-Checkout unberuehrt (PFLICHT, zwei Beweise):**
|
- **Live-Checkout unberuehrt (PFLICHT, zwei Beweise):**
|
||||||
- `git -C ~/mission-control-v2 status --porcelain -uno` **muss LEER sein** (Beweis, dass du
|
- `git -C ~/mission-control-v2 status --porcelain -uno` **muss LEER sein** (Beweis, dass du
|
||||||
wirklich nur im Worktree gearbeitet hast — ein gruener Health-curl allein reicht NICHT, weil
|
wirklich nur im Worktree gearbeitet hast — ein gruener Health-curl allein reicht NICHT, weil
|
||||||
der laufende Dienst den alten Code im RAM haelt und eine schmutzige Datei auf der Platte nicht
|
der laufende Dienst den alten Code im RAM haelt und eine schmutzige Datei auf der Platte nicht
|
||||||
bemerkt).
|
bemerkt).
|
||||||
- `curl -sf http://127.0.0.1:9001/api/health` muss gruen bleiben.
|
- `curl -sf http://127.0.0.1:9001/api/health` muss gruen bleiben.
|
||||||
- Ist `git status` NICHT leer: die fremden Aenderungen im Live-Checkout mit
|
- Ist `git status` NICHT leer: die fremden Aenderungen im Live-Checkout mit
|
||||||
`git -C ~/mission-control-v2 checkout -- <datei>` zuruecknehmen (deine Arbeit liegt sicher im
|
`git -C ~/mission-control-v2 checkout -- <datei>` zuruecknehmen (deine Arbeit liegt sicher im
|
||||||
Worktree) und im Vorschlag ehrlich erwaehnen.
|
Worktree) und im Vorschlag ehrlich erwaehnen.
|
||||||
|
|
||||||
### 5. Zwei-Kritiker-Gate ueber den GESAMTEN Diff (Pflicht)
|
### 5. Zwei-Kritiker-Gate ueber den GESAMTEN Diff (Pflicht)
|
||||||
Zwei Zweitgutachter mit je anderer Staerke lesen den zusammengesetzten Diff gegen — aus demselben
|
Zwei Zweitgutachter mit je anderer Staerke lesen den zusammengesetzten Diff gegen — aus demselben
|
||||||
Grund wie in der Werkstatt („wer seine eigenen Hausaufgaben benotet, stimmt sich selbst zu"). **NICHT
|
Grund wie in der Werkstatt („wer seine eigenen Hausaufgaben benotet, stimmt sich selbst zu"). **NICHT
|
||||||
`delegate_task`** (laeuft ueber `heavy` → 64K-Floor + Warm-Set-Kollision).
|
`delegate_task`** (laeuft ueber `heavy` → 64K-Floor + Warm-Set-Kollision).
|
||||||
|
|
||||||
**Zuerst ALLES stagen** — sonst fehlen NEUE Dateien im Diff (haeufigste Falle: `git diff` ignoriert
|
**Zuerst ALLES stagen** — sonst fehlen NEUE Dateien im Diff (haeufigste Falle: `git diff` ignoriert
|
||||||
untracked Dateien, die Kritiker bekaemen einen LEEREN Diff und wuerden alles blind ablehnen):
|
untracked Dateien, die Kritiker bekaemen einen LEEREN Diff und wuerden alles blind ablehnen):
|
||||||
```
|
```
|
||||||
git -C ~/.hermes/worktrees/orch-<slug> add -A
|
git -C ~/.hermes/worktrees/orch-<slug> add -A
|
||||||
```
|
```
|
||||||
Dann die Eingabe EINMAL bauen (mit `--cached`, damit die neuen Dateien drin sind):
|
Dann die Eingabe EINMAL bauen (mit `--cached`, damit die neuen Dateien drin sind):
|
||||||
```
|
```
|
||||||
EINGABE="AUFTRAG (im Wortlaut):
|
EINGABE="AUFTRAG (im Wortlaut):
|
||||||
<der urspruengliche Auftrag>
|
<der urspruengliche Auftrag>
|
||||||
|
|
||||||
ZERLEGUNG (deine Teil-Tasks, je 1 Zeile):
|
ZERLEGUNG (deine Teil-Tasks, je 1 Zeile):
|
||||||
<1..n>
|
<1..n>
|
||||||
|
|
||||||
GIT-DIFF des Worktrees:
|
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.**
|
||||||
|
|
||||||
**Gate-Regel (streng):** Sagt AUCH NUR EINER `ABGELEHNT` → nachbessern (max. 2 Runden: den
|
**Gate-Regel (streng):** Sagt AUCH NUR EINER `ABGELEHNT` → nachbessern (max. 2 Runden: den
|
||||||
betroffenen Teil-Task neu an den Worker, integrieren, BEIDE Kritiker erneut). `FREIGABE-MIT-VORBEHALT`
|
betroffenen Teil-Task neu an den Worker, integrieren, BEIDE Kritiker erneut). `FREIGABE-MIT-VORBEHALT`
|
||||||
→ die Vorbehalte adressieren oder im Vorschlag ehrlich ausweisen (NICHT wegdiskutieren). Nur wenn
|
→ die Vorbehalte adressieren oder im Vorschlag ehrlich ausweisen (NICHT wegdiskutieren). Nur wenn
|
||||||
BEIDE tragen → Vorschlag. Beide Urteile KONKRET in den Vorschlag (welcher Kritiker was sagte). Faellt
|
BEIDE tragen → Vorschlag. Beide Urteile KONKRET in den Vorschlag (welcher Kritiker was sagte). Faellt
|
||||||
ein Kritiker technisch aus (`FEHLGESCHLAGEN`/`ABGESCHNITTEN`) → einmal wiederholen, sonst „⚠ Kritik X
|
ein Kritiker technisch aus (`FEHLGESCHLAGEN`/`ABGESCHNITTEN`) → einmal wiederholen, sonst „⚠ Kritik X
|
||||||
fiel aus" vermerken (NICHT als Freigabe zaehlen).
|
fiel aus" vermerken (NICHT als Freigabe zaehlen).
|
||||||
|
|
||||||
> Der schwere **Chef-Gutachter (`gpt-oss`)** laeuft NICHT in diesem heissen Pfad — er muesste 60 GB
|
> Der schwere **Chef-Gutachter (`gpt-oss`)** laeuft NICHT in diesem heissen Pfad — er muesste 60 GB
|
||||||
> kalt laden und Lucys Warm-Set stoeren. Er prueft separat NACHTS im Leerlauf ueber die offenen
|
> kalt laden und Lucys Warm-Set stoeren. Er prueft separat NACHTS im Leerlauf ueber die offenen
|
||||||
> Orchestrator-Vorschlaege (eigener Cron), wo Ladezeit + Warm-Set-Umbau verkraftbar sind.
|
> Orchestrator-Vorschlaege (eigener Cron), wo Ladezeit + Warm-Set-Umbau verkraftbar sind.
|
||||||
- `ABGELEHNT` / berechtigte `FREIGABE-MIT-VORBEHALT` → nachbessern (max. 2 Runden: den betroffenen
|
- `ABGELEHNT` / berechtigte `FREIGABE-MIT-VORBEHALT` → nachbessern (max. 2 Runden: den betroffenen
|
||||||
Teil-Task neu an den Worker geben, integrieren, erneut gaten). Bleibt es kritisch: Kritik in den
|
Teil-Task neu an den Worker geben, integrieren, erneut gaten). Bleibt es kritisch: Kritik in den
|
||||||
Vorschlag schreiben, Branch bleibt liegen, Entscheidung dem Commander.
|
Vorschlag schreiben, Branch bleibt liegen, Entscheidung dem Commander.
|
||||||
- `FREMDBLICK FEHLGESCHLAGEN`/`ABGESCHNITTEN` → einmal wiederholen; sonst im Vorschlag ehrlich
|
- `FREMDBLICK FEHLGESCHLAGEN`/`ABGESCHNITTEN` → einmal wiederholen; sonst im Vorschlag ehrlich
|
||||||
„⚠ Fremd-Review fiel aus — UNGEPRUEFT" vermerken (NICHT als Freigabe behandeln).
|
„⚠ Fremd-Review fiel aus — UNGEPRUEFT" vermerken (NICHT als Freigabe behandeln).
|
||||||
|
|
||||||
### 6. Telegram-Vorschlag (in LUCYS Stimme)
|
### 6. Telegram-Vorschlag (in LUCYS Stimme)
|
||||||
`bash ~/mission-control-v2/deploy/notify.sh -s "[Orchestrator]" "<text>"` — an den Commander, nicht
|
`bash ~/mission-control-v2/deploy/notify.sh -s "[Orchestrator]" "<text>"` — an den Commander, nicht
|
||||||
als anonymer Job: erst 1–2 Saetze, was gebaut wurde und was er jetzt entscheiden soll; dann knapp:
|
als anonymer Job: erst 1–2 Saetze, was gebaut wurde und was er jetzt entscheiden soll; dann knapp:
|
||||||
- die **Zerlegung** (welcher Teil-Task an welches Worker-Modell ging),
|
- die **Zerlegung** (welcher Teil-Task an welches Worker-Modell ging),
|
||||||
- geaenderte/neue Dateien · Kern des Diffs,
|
- geaenderte/neue Dateien · Kern des Diffs,
|
||||||
- Self-Gate-Ergebnis · Kritiker-Urteil KONKRET ausgeschrieben (nicht nur „ok"),
|
- Self-Gate-Ergebnis · Kritiker-Urteil KONKRET ausgeschrieben (nicht nur „ok"),
|
||||||
- Branch-Name · Frage „merge oder verwerfen?"
|
- Branch-Name · Frage „merge oder verwerfen?"
|
||||||
|
|
||||||
### 7. Nichts loeschen
|
### 7. Nichts loeschen
|
||||||
Worktree + Branch + rohe Worker-Ausgaben bleiben liegen, bis der Commander entschieden hat.
|
Worktree + Branch + rohe Worker-Ausgaben bleiben liegen, bis der Commander entschieden hat.
|
||||||
|
|
||||||
## Kontext-Uebergabe — hier geht Orchestrierung am ehesten schief
|
## Kontext-Uebergabe — hier geht Orchestrierung am ehesten schief
|
||||||
Jeder Worker startet **frisch und zustandslos**: kein Gedaechtnis der bisherigen Schritte, kein
|
Jeder Worker startet **frisch und zustandslos**: kein Gedaechtnis der bisherigen Schritte, kein
|
||||||
Zugriff aufs Repo. Er sieht NUR, was du ihm auf stdin gibst. Deshalb:
|
Zugriff aufs Repo. Er sieht NUR, was du ihm auf stdin gibst. Deshalb:
|
||||||
- Gib ihm den **relevanten Ausschnitt** mit — die betroffene Datei (oder den betroffenen Block),
|
- Gib ihm den **relevanten Ausschnitt** mit — die betroffene Datei (oder den betroffenen Block),
|
||||||
benachbarte Muster zum Nachahmen, die genaue Signatur/den Namen, den er treffen muss, und bei
|
benachbarte Muster zum Nachahmen, die genaue Signatur/den Namen, den er treffen muss, und bei
|
||||||
Folge-Schritten die bereits fertigen Artefakte, auf die er aufbaut.
|
Folge-Schritten die bereits fertigen Artefakte, auf die er aufbaut.
|
||||||
- Sei praezise beim Zielartefakt: „Gib den VOLLSTAENDIGEN Inhalt der neuen Datei `X` aus" bzw. „Gib
|
- Sei praezise beim Zielartefakt: „Gib den VOLLSTAENDIGEN Inhalt der neuen Datei `X` aus" bzw. „Gib
|
||||||
einen unified Diff gegen den folgenden Stand von `Y` aus".
|
einen unified Diff gegen den folgenden Stand von `Y` aus".
|
||||||
- Zu wenig Kontext → der Worker raet oder meldet `FEHLT:`. Zu viel unnoetiger Kontext → er verliert
|
- Zu wenig Kontext → der Worker raet oder meldet `FEHLT:`. Zu viel unnoetiger Kontext → er verliert
|
||||||
den Fokus. Ziel: das kleinste, das den Teil-Task eindeutig macht.
|
den Fokus. Ziel: das kleinste, das den Teil-Task eindeutig macht.
|
||||||
|
|
||||||
## Nach dem Commander-Entscheid (kommt als neuer Auftrag)
|
## Nach dem Commander-Entscheid (kommt als neuer Auftrag)
|
||||||
- „verwerfen" → `git worktree remove ~/.hermes/worktrees/orch-<slug> --force && git branch -D orchestrator/<slug>`
|
- „verwerfen" → `git worktree remove ~/.hermes/worktrees/orch-<slug> --force && git branch -D orchestrator/<slug>`
|
||||||
(+ `rm -rf ~/.hermes/worktrees/orch-<slug>-work`; Remote-Branch loeschen, falls gepusht).
|
(+ `rm -rf ~/.hermes/worktrees/orch-<slug>-work`; Remote-Branch loeschen, falls gepusht).
|
||||||
- „merge" → das Mergen nach main + Deploy macht der PC/Claude (Frontend-Builds gibt es nur dort).
|
- „merge" → das Mergen nach main + Deploy macht der PC/Claude (Frontend-Builds gibt es nur dort).
|
||||||
Du pushst NIE nach main.
|
Du pushst NIE nach main.
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: pruefstand
|
||||||
|
description: "Holt Prüfstand-Tickets aus dem Kanban-Board, testet neue LLM-Modelle auf Kompatibilität und Systemressourcen und meldet die Ergebnisse via Telegram."
|
||||||
|
version: 1.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [pruefstand, evaluation, models, mc2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Prüfstand (Model Evaluation)
|
||||||
|
|
||||||
|
Der Prüfstand-Skill dient dazu, neue LLMs oder Tooling-Upgrades (oft aus dem Trend-Radar stammend) autonom auf der Box zu evaluieren, bevor sie ins Live-System übernommen werden.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
1. **Ticket Fetching**: Liest das nächste anstehende Ticket in der Kanban Ideen-Queue, welches als "Prüfstand" getaggt ist.
|
||||||
|
2. **Download & Setup**: Lädt das beschriebene Modell (z.B. via `huggingface-cli` oder llama.cpp) in ein isoliertes Test-Verzeichnis.
|
||||||
|
3. **Smoke-Tests**: Führt vordefinierte Benchmarks oder Prompts gegen das Modell aus und überwacht den RAM/VRAM-Bedarf.
|
||||||
|
4. **Bewertung**: Wenn das Modell die Kriterien erfüllt (schnell genug, kein OOM), wird es als "Bestanden" markiert und ein Integrations-Vorschlag erstellt.
|
||||||
|
5. **Report**: Der Commander erhält einen Telegram-Bericht mit den Metriken und der Entscheidung.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: review
|
||||||
|
description: "Ersetzt das alte fremdblick.sh Skript. Ein dedizierter Code-Review-Skill, der über ein fremdes Modell als 'Advocatus Diaboli' Patches hart prüft und nach Reproduzierbarkeit bewertet."
|
||||||
|
version: 1.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux, macos, windows]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [review, code-quality, pr, mc2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code Review (Fremdblick)
|
||||||
|
|
||||||
|
Dieser Skill delegiert das Code-Review an ein separates, kritisches LLM-Modell (Advocatus Diaboli), um Fehler, Architektur-Schwächen und blinde Flecken zu finden.
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
1. **Diff einlesen**: Nimmt den aktuellen Git-Diff oder den Patch-Inhalt.
|
||||||
|
2. **Kritische Analyse**: Prüft den Code auf Edge-Cases, Security, Performance und Architektonische Richtigkeit.
|
||||||
|
3. **Reproduzierbarkeits-Prüfung**: Das Modell muss den Fehlerfall gedanklich nachstellen und beweisen, ob der Fix ihn wirklich behebt.
|
||||||
|
4. **Urteil fällen**:
|
||||||
|
- `ABGELEHNT`: Fehler nicht behoben oder neue Bugs eingeführt.
|
||||||
|
- `FREIGABE-MIT-VORBEHALT`: Funktioniert, aber unsauber oder fehlende Kommentare.
|
||||||
|
- `FREIGABE`: Wasserdicht.
|
||||||
|
5. **Ergebnis melden**: Das Urteil wird an den Commander zurückgespielt (oft kombiniert mit der Werkstatt).
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: trend_radar
|
||||||
|
description: "Autonomer KI-Technologie-Scout. Recherchiert im Web nach tagesaktuellen Trends, passt seine eigenen Quellen an und sendet Telegram-Reports."
|
||||||
|
version: 2.0.0
|
||||||
|
author: MC2
|
||||||
|
platforms: [linux, macos, windows]
|
||||||
|
metadata:
|
||||||
|
hermes:
|
||||||
|
tags: [research, radar, trends, mc2, self-modifying]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Trend Radar (Autonomous & Self-Modifying)
|
||||||
|
|
||||||
|
Du bist der KI-Technologie-Scout von Mission Control 2.0. Dein Ziel ist es, tagesaktuelle Trends (Modelle, Tools, Hardware-Performance) aufzuspüren, die für uns relevant sind (Fokus: AMD Hardware, Vulkan/ROCm, kleine aber sehr smarte Open-Source-Modelle).
|
||||||
|
|
||||||
|
## Strikte Hardware-Limits (SEHR WICHTIG)
|
||||||
|
Unsere AI-Box (Bosgame M5 / Strix Halo) hat **exakt 128 GB Shared RAM** (ca. 122.7 GB nutzbar)!
|
||||||
|
Das bedeutet:
|
||||||
|
- **Ignoriere gigantische Modelle:** Modelle mit 200B+, 700B oder 2T Parametern (wie Qwen 3.8 Max, GLM-5.5) sind für uns absolut nutzlos, da sie lokal niemals laufen werden.
|
||||||
|
- **Größen-Limit:** Ein Modell darf im GGUF-Quant-Format (z.B. Q4_K_M) maximal **ca. 100-110 GB** groß sein. Das entspricht meist Modellen bis maximal ~150B Parametern (bei dichten Modellen) oder etwas mehr bei MoE-Modellen.
|
||||||
|
- **Fokus auf Local LLMs:** Closed-API-Modelle interessieren uns nur am Rande. Konzentriere dich auf Modelle, die wir herunterladen und lokal in `llama.cpp` oder `Ollama` auf AMD-Hardware ausführen können. Sortiere alles rigoros aus, was zu groß für 128 GB RAM ist!
|
||||||
|
|
||||||
|
## Deine Fähigkeiten & Autorisierung
|
||||||
|
Du hast die ausdrückliche Erlaubnis zur **Selbst-Mutation**. Du bist nicht nur ein Leser, du bist ein adaptiver Agent:
|
||||||
|
- Wenn du neue, wertvolle Quellen (z.B. neue Subreddits, News-Seiten oder Repositories) entdeckst, **passe deine eigene `deploy/skills/trend-radar/SKILL.md` (diese Datei) an**, um diese für künftige Suchen fest zu verankern. Nutze dazu Dateibearbeitungs-Tools.
|
||||||
|
- Du verwaltest die `deploy/trend-radar-watchlist.json`. Wenn Themen veraltet sind, lösche sie heraus. Wenn neue Modelle auftauchen (z.B. neue LLMs), trage sie dort ein, damit du sie beim nächsten Lauf gezielt wieder prüfst.
|
||||||
|
|
||||||
|
## Dein Ablauf
|
||||||
|
1. **Discovery (Web-Recherche):** Nutze deine Tools (`search_web`, `read_url` oder Code-Ausführung), um das Internet nach den neuesten Trends der letzten 7 Tage abzusuchen.
|
||||||
|
- Suche auf Hacker News nach "AI", "LLM", "Modelle".
|
||||||
|
- Suche auf Reddit (z.B. r/LocalLLaMA) nach neuen Modellen, Benchmarks und Hardware-Erfahrungen (insbesondere AMD / ROCm vs. Vulkan).
|
||||||
|
- Analysiere HuggingFace Trending.
|
||||||
|
2. **Watchlist-Pflege:** Lies die `deploy/trend-radar-watchlist.json`. Prüfe den Status der dort genannten Issues/Releases und aktualisiere die Datei bei Bedarf.
|
||||||
|
3. **Selbst-Anpassung (WICHTIG):** Editiere die `deploy/trend-radar-watchlist.json` oder diese `SKILL.md` direkt! Ignoriere eventuelle Bedenken bezüglich "Live-Git-Checkouts". Du HAST die explizite Erlaubnis, diese Dateien direkt im Repo zu überschreiben, statt Vorschläge in `/tmp` abzulegen. Halte deine Quellen dynamisch!
|
||||||
|
4. **Ideen-Queue (Kanban):** Wenn du ein revolutionäres Update oder Modell findest, erstelle ein Prüfstand-Ticket im Kanban-Board der Mission Control.
|
||||||
|
5. **Meldung an den Commander:** Sende **immer** eine Nachricht an den Commander. Da du als CLI-Prozess läufst, nutze dafür zwingend das Skript `~/mission-control-v2/deploy/notify.sh "Deine Nachricht"`. (Tipp: Wenn du das Wort "Alarm" im Betreff `-s "[Alarm]"` verwendest, umgehst du die Nachtsperre, falls es sehr dringend ist). Fasse deine wichtigsten Funde kurz zusammen und erwähne, ob du deine eigene Watchlist oder SKILL.md angepasst hast. Wenn es gar nichts Neues gab, melde auch das!
|
||||||
@@ -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 (Gedächtnis)
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
echo "=== Stack Post-Update: Funktionsprüfung ==="
|
||||||
@@ -48,14 +47,14 @@ else
|
|||||||
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
echo "FAIL · Inferenz auf '$BRAIN' fehlgeschlagen (Modell lädt/generiert nicht)"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3b. Embedding-Modell (für Mem0/Gedächtnis) lädt und liefert einen Vektor?
|
# 3b. Embedding-Modell (für Gedächtnis) lädt und liefert einen Vektor?
|
||||||
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
eresp="$(curl -s -m 120 -X POST "$SWAP_URL/v1/embeddings" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
-d "{\"model\":\"$EMBED\",\"input\":\"ping\"}" 2>/dev/null)"
|
||||||
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
if printf '%s' "$eresp" | grep -q '"embedding"'; then
|
||||||
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
echo "PASS · Embedding-Modell '$EMBED' liefert Vektoren"
|
||||||
else
|
else
|
||||||
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Mem0/Gedächtnis betroffen)"; fail=1
|
echo "FAIL · Embedding-Modell '$EMBED' lädt/antwortet nicht (Gedächtnis betroffen)"; fail=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
# 4. MC2 selbst gesund (Engine + Gateway erreichbar)?
|
||||||
@@ -85,13 +84,6 @@ if systemctl --user is-enabled --quiet mc2-steward 2>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Mem0-Sidecar (Gedächtnis) erreichbar?
|
|
||||||
if curl -sf -m 5 "$MEM0_URL/health" >/dev/null 2>&1; then
|
|
||||||
echo "PASS · Mem0-Sidecar erreichbar"
|
|
||||||
else
|
|
||||||
echo "FAIL · Mem0-Sidecar NICHT erreichbar"; fail=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$fail" -eq 0 ]; then
|
if [ "$fail" -eq 0 ]; then
|
||||||
echo "=== STACK OK ✓ ==="
|
echo "=== STACK OK ✓ ==="
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_hinweis": "Watch-Liste des Trend-Radars (Sa 05:15) — mechanisch prüfbare Bedingungen statt Prosa-Erinnerungen. Neue Einträge einfach anhängen; der Radar prüft github_issue (offen/zu), github_release (neuer Tag) und url_zeilen (Zeilen mit Suchmuster aus einer Roh-URL).",
|
"_hinweis": "Living Watchlist des Trend-Radars. Diese Datei MUSS autonom vom Trend-Radar Agenten umgeschrieben werden, wenn er neue Quellen findet oder alte obsolet werden.",
|
||||||
"eintraege": [
|
"eintraege": [
|
||||||
{
|
{
|
||||||
"key": "mmq-prefill-gfx1151",
|
"key": "mmq-prefill-gfx1151",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Mem0, Voice) altern
|
# Venv-Audit (monatlich, 17.07.2026): die Python-Nebendienste (Voice) altern
|
||||||
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
# eingefroren — kein Auto-Update-Kanal deckt sie ab. Dieser Audit MELDET nur
|
||||||
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
# (bewusst kein Auto-Update: Python-Deps brechen gern):
|
||||||
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
# je venv: veraltete Pakete zählen (pip list --outdated) + bekannte
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
# idempotent je Monat). Läuft als Hermes-Cron --no-agent (stdout = Telegram).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
VENVS=("$HOME/.mem0/venv:mem0" "$HOME/.voice/venv:voice")
|
VENVS=("$HOME/.voice/venv:voice")
|
||||||
AUDIT_VENV="$HOME/.venv-audit"
|
AUDIT_VENV="$HOME/.venv-audit"
|
||||||
HERMES="$HOME/.local/bin/hermes"
|
HERMES="$HOME/.local/bin/hermes"
|
||||||
MONAT="$(date '+%Y-%m')"
|
MONAT="$(date '+%Y-%m')"
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
# Eingehängt als ExecStartPost=-/usr/local/bin/llama-swap-warmup.sh in llama-swap.
|
||||||
#
|
#
|
||||||
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
# `fast` = das Agent-Hirn (Qwen3.6-35B-A3B), `embed` = das Embedding-Modell
|
||||||
# (Qwen3-Embedding-0.6B, für Mem0/Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
# (Qwen3-Embedding-0.6B, für Gedächtnis). Die Augen (vision/VL-30B) sind seit 07.07.
|
||||||
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
# ON-DEMAND (User-Entscheid) und gehören NICHT mehr ins Warm-Set — die Lucy-App wärmt sie
|
||||||
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
# beim Start selbst an. Modelle werden NICHT automatisch zusammen geladen — jedes einzeln
|
||||||
# anstoßen, sonst bleibt es kalt.
|
# anstoßen, sonst bleibt es kalt.
|
||||||
|
|||||||
+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}"
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ delegation:
|
|||||||
- **Kaputte/Cloud-Tools global abschalten** (sonst Endlos-Loops, siehe Memory `hermes-thrash-rootcause-fix`):
|
- **Kaputte/Cloud-Tools global abschalten** (sonst Endlos-Loops, siehe Memory `hermes-thrash-rootcause-fix`):
|
||||||
```yaml
|
```yaml
|
||||||
agent:
|
agent:
|
||||||
disabled_toolsets: [browser, vision, computer_use, image_gen, tts, video, video_gen, memory]
|
disabled_toolsets: [vision, computer_use, image_gen, tts, video, video_gen]
|
||||||
memory:
|
memory:
|
||||||
memory_enabled: false
|
memory_enabled: true
|
||||||
user_profile_enabled: false
|
user_profile_enabled: true
|
||||||
```
|
```
|
||||||
⚠️ `hermes tools disable <x>` wirkt nur für die cli-Plattform, **nicht den api_server** — nutze
|
⚠️ `hermes tools disable <x>` wirkt nur für die cli-Plattform, **nicht den api_server** — nutze
|
||||||
`agent.disabled_toolsets` (gilt für ALLE Plattformen).
|
`agent.disabled_toolsets` (gilt für ALLE Plattformen).
|
||||||
- **Behalten:** terminal/shell, file, code_execution, skills, todo, session_search, clarify,
|
- **Behalten:** terminal/shell, file, code_execution, skills, todo, session_search, clarify,
|
||||||
delegation, cronjob, web. `approvals: auto` (rein lokal).
|
delegation, cronjob, web, browser, memory. `approvals: auto` (rein lokal).
|
||||||
- **MCP-Server** (`mcp_servers` in config) — laufen über die v2-venv (hat das `mcp`-Modul nach pip install):
|
- **MCP-Server** (`mcp_servers` in config) — laufen über die v2-venv (hat das `mcp`-Modul nach pip install):
|
||||||
- geteiltes Gedächtnis: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_memory.py` (Env `MC_URL=http://127.0.0.1:9001`)
|
- geteiltes Gedächtnis: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_memory.py` (Env `MC_URL=http://127.0.0.1:9001`)
|
||||||
- Stack-Management: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_mc.py` (Env `MC_URL=http://127.0.0.1:9001`)
|
- Stack-Management: `~/mission-control-v2/backend/.venv/bin/python ~/mission-control-v2/mcp/mcp_mc.py` (Env `MC_URL=http://127.0.0.1:9001`)
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -88,17 +88,6 @@ Neue Fallen: hier eintragen, mit Datum und Symptom._
|
|||||||
- **Werkstatt-Gate:** `git -C ~/mission-control-v2 status --porcelain -uno` MUSS leer sein
|
- **Werkstatt-Gate:** `git -C ~/mission-control-v2 status --porcelain -uno` MUSS leer sein
|
||||||
(curl /api/health allein ist ein Loch — alter Code läuft im RAM weiter).
|
(curl /api/health allein ist ein Loch — alter Code läuft im RAM weiter).
|
||||||
|
|
||||||
## Mem0 / Gedächtnis
|
|
||||||
|
|
||||||
- `mem0_service/requirements.txt` ist GEPINNT (wir nutzen Interna: NoThink-Swap, Roh-Chroma) —
|
|
||||||
blindes Upgrade bricht still; vorher/nachher `smoke_test.py`, Prozedur `docs/UPGRADE.md`.
|
|
||||||
- Qwen3.6 ist ein Thinking-Modell: JSON-Extraktion nur mit `enable_thinking:false`
|
|
||||||
(NoThinkLLM im Sidecar), sonst leere Extraktion.
|
|
||||||
- **Delete-Resurrection:** gelöschter Fakt kann beim nächsten learn() wieder auftauchen,
|
|
||||||
solange die Original-Nachricht in history.db liegt (mem0-Design, kein Bug).
|
|
||||||
- `sync_turn` läuft im Hintergrund → in kurzlebigen Prozessen (`hermes -z`) verpufft das
|
|
||||||
Lernen; nur langlebige Sessions lernen zuverlässig.
|
|
||||||
- Frontend: `reagraph` auf 4.22.0 gepinnt (4.23+ zieht React 19 → Crash).
|
|
||||||
|
|
||||||
## Lucy / Windows-PC
|
## Lucy / Windows-PC
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
# Gedächtnis-Bereiche: alltag vs. projekt (Stand 19.07.2026)
|
|
||||||
|
|
||||||
## Warum
|
|
||||||
Lucys mem0 sammelte Coding-/Ops-Schutt (execute_code-Bugs, Session-Zahlen,
|
|
||||||
Hardware-Debugging) — Rauschen im Alltags-Recall. Entscheid (Commander, 19.07.):
|
|
||||||
**KEIN zweites mem0** (das Coding-Gedächtnis ist das Repo: AGENTS.md, Code, Git,
|
|
||||||
Karten). Stattdessen bekommt jeder Fakt ein Bereichs-Feld — die Trennung liegt in
|
|
||||||
den Daten, nicht in einer zweiten Datenbank.
|
|
||||||
|
|
||||||
## Was seit 19.07. existiert (gebaut, live)
|
|
||||||
- `mem0_service/app.py` → `classify_facts()` ordnet jeden neu gelernten Fakt
|
|
||||||
zusätzlich einem **Bereich** zu: `alltag` (Nutzer als Person: Vorlieben, Leben,
|
|
||||||
Arbeitsweise, Grundsatz-Entscheidungen) oder `projekt` (Technik: Code, Modelle,
|
|
||||||
Server, Infrastruktur, Fehler-/Projektstände). Gespeichert als Metadatum
|
|
||||||
`bereich`, ausgegeben in jedem `/memory`-Item.
|
|
||||||
- `GET /memory?bereich=alltag|projekt` filtert. **Alt-Fakten ohne Feld laufen bei
|
|
||||||
jedem Filter MIT** (nichts verschwindet durch die Umstellung).
|
|
||||||
- Quellen-Hygiene: Die IDE-Welt (Zed+OpenCode) schreibt seit 19.07. GAR NICHT mehr
|
|
||||||
ins mem0; Junk-Guard + Lern-Bündelung + monatliche Konsolidierung (3., 06:40)
|
|
||||||
laufen. Der Neuzugang an Schutt sollte dadurch bereits stark sinken.
|
|
||||||
|
|
||||||
## Selbstprüfung nach 4 Wochen (Cron „gedaechtnis-check", einmalig Mitte August)
|
|
||||||
Auftrag an Lucy: Die 50 neuesten Fakten ansehen (`GET /api/memory` sortiert
|
|
||||||
neueste zuerst). Frage: Wie viele davon sind Tool-/Fehler-/Momentaufnahmen-Schutt
|
|
||||||
(Muster: Tool-Namen, Fehlermeldungen, Session-/Commit-Zahlen, „blockiert/behoben"-
|
|
||||||
Zustände)? **> 10 von 50 → Werkstatt-Karte anlegen** mit Verweis auf dieses
|
|
||||||
Dokument und den Bauauftrag unten. Sonst: kurze Entwarnung an den Commander.
|
|
||||||
|
|
||||||
## Bauauftrag FALLS nötig: Recall-Filter (klein, KEINE neue Schicht)
|
|
||||||
Ziel: In Alltags-Gesprächen streut kein Projekt-Schutt in den Recall.
|
|
||||||
Umsetzung in `hermes/plugins/mc2-memory/__init__.py`, Funktion `prefetch()`:
|
|
||||||
1. Neuer Env-Schalter `MC2_MEMORY_RECALL_BEREICH` (leer = aus, Default).
|
|
||||||
2. Wenn gesetzt: an den Request `params={"q": q, "bereich": <wert>}` anhängen —
|
|
||||||
der Sidecar filtert serverseitig (Fakten ohne Feld laufen mit, siehe oben).
|
|
||||||
3. Setzen NUR für Lucys Voice-/Chat-Gateway (Umgebung der hermes-gateway-Unit),
|
|
||||||
NICHT für Worker-Profile — Worker sollen Projekt-Fakten ja sehen.
|
|
||||||
4. Verify: zwei Test-Learns (ein Alltags-, ein Projekt-Fakt), dann `/memory?q=…`
|
|
||||||
mit und ohne Filter vergleichen; Latenz unverändert (< 1 s Overhead).
|
|
||||||
Aufwand: < 1 Stunde. Regeln: deploy braucht Commander-Ja; Plugin-Änderung =
|
|
||||||
Gateway-Neustart.
|
|
||||||
|
|
||||||
## Bewusst NICHT tun
|
|
||||||
- Kein zweites mem0/zweite Collection (doppelte Pflege, doppelte Konsolidierung).
|
|
||||||
- Keinen Alt-Bestand automatisch umtaggen (die Monats-Konsolidierung räumt ohnehin;
|
|
||||||
der Filter behandelt ungetaggte Fakten konservativ als „immer sichtbar").
|
|
||||||
+33
-41
@@ -1,7 +1,7 @@
|
|||||||
# Stack-Wahrheiten — Infrastruktur, Dienste, Modelle
|
# Stack-Wahrheiten — Infrastruktur, Dienste, Modelle
|
||||||
|
|
||||||
_Live gegen die Box verifiziert am **10.07.2026** (Dienste-Liste, llama-swap-Config, hermes
|
_Live gegen die Box verifiziert am **19.08.2026** (Dienste-Liste, llama-swap-Config, hermes
|
||||||
--version, /api/health). Bei Widerspruch zu älteren Docs (SAVEPOINT.md, docs/memory/) gewinnt
|
--version v0.20.4, llama.cpp b10502, /api/health). Bei Widerspruch zu älteren Docs gewinnt
|
||||||
diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
||||||
|
|
||||||
## Maschinen & Zugänge
|
## Maschinen & Zugänge
|
||||||
@@ -24,57 +24,49 @@ diese Datei. Wer sie ändert: erst messen, dann schreiben._
|
|||||||
- **Wissens-Vault:** `~/wissens-vault/` auf der Box (eigenes git; Lucys Lernschicht:
|
- **Wissens-Vault:** `~/wissens-vault/` auf der Box (eigenes git; Lucys Lernschicht:
|
||||||
Träume, Radar-Funde, **Eigenbau-Landkarte**, Skill-Kandidaten). Im MC2-UI als Wissens-Tab.
|
Träume, Radar-Funde, **Eigenbau-Landkarte**, Skill-Kandidaten). Im MC2-UI als Wissens-Tab.
|
||||||
|
|
||||||
## Dienste auf der Box (live 10.07.2026)
|
## Dienste auf der Box (live 19.08.2026)
|
||||||
|
|
||||||
| Dienst | Port | Zweck |
|
| Dienst | Port | Zweck |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `llama-swap` (System-Dienst) | `:8080` | Modell-Router; Engine = llama.cpp **Vulkan/RADV** (`/opt/llamacpp-vulkan`) |
|
| `llama-swap` (System-Dienst) | `:8080` | Modell-Router; Engine = llama.cpp **Vulkan/RADV** (`/opt/llamacpp-vulkan`, b10502) |
|
||||||
| `mission-control-2` (User) | `:9001` | MC2 Backend+Frontend; `/v1` = OpenAI-Gateway; `/hermes-ui/` = Desktop-Gateway-Proxy |
|
| `mission-control-2` (User) | `:9001` | MC2 Cockpit (FastAPI + React Frontend) & Steuerpult |
|
||||||
| `hermes-gateway` (User) | `:8642` | Hermes Agent API (Lucys Lane) |
|
| `mc2-gateway` (User) | `:9010` | `/v1`-Datenpfad (model:auto Routing + native Bildweiche) |
|
||||||
| `hermes-builtin-ui` (User) | `:9119` (loopback) | Eingebaute Hermes-GUI; LAN-Zugang NUR via MC2-Proxy `/hermes-ui/` (statischer Token im systemd-Drop-in) |
|
| `mc2-steward` (User) | — | Hintergrundwächter (Re-Warm, Health-Sentry, Mem0-Dedupe) |
|
||||||
| `mem0-service` (User) | `:8765` (loopback) | Mem0-Sidecar (Gedächtnis), Py 3.12 `~/.mem0/venv` |
|
| `hermes-gateway` (User) | `:8642` | Hermes Agent Core API (v0.20.4, Bot Mode) |
|
||||||
| `voice-service` (User) | `:8650` | STT (Parakeet/Whisper) + Turn-Check für Lucy |
|
| `hermes-builtin-ui` (User) | `:9119` (loopback) | Eingebaute Hermes-GUI; LAN-Zugang via MC2-Proxy `/hermes-ui/` |
|
||||||
|
| `mem0-service` (User) | `:8765` | Semantischer Chroma-Gedächtnis-Sidecar |
|
||||||
|
| `voice-service` (User) | `:8650` | STT (faster-whisper) + TTS (Piper/Chatterbox) |
|
||||||
| `box-console` (User) | `:7682` | ttyd-Login-Shell hinter MC2-Proxy |
|
| `box-console` (User) | `:7682` | ttyd-Login-Shell hinter MC2-Proxy |
|
||||||
|
|
||||||
**Stillgelegt (Diät 09.07.):** `hermes-terminal` (:7681) und `hermes-webui` (:8787) — nicht
|
**Stillgelegt:** `hermes-terminal` (:7681), `hermes-webui` (:8787) und alter `governor` (:8100).
|
||||||
wiederbeleben. ⚠️ User-Units per SSH nur sichtbar mit
|
⚠️ User-Units per SSH nur sichtbar mit `export XDG_RUNTIME_DIR=/run/user/$(id -u)` vor `systemctl --user`.
|
||||||
`export XDG_RUNTIME_DIR=/run/user/$(id -u)` vor `systemctl --user`.
|
|
||||||
|
|
||||||
## Modell-Stack (llama-swap, live 10.07.2026)
|
## Modell-Stack (llama-swap, live 19.08.2026)
|
||||||
|
|
||||||
**Warm-Set (Gruppe `brains`, swap:false, persistent:true, alle ttl 0):**
|
**Warm-Set (Gruppe `brains`, swap:false, persistent:false, 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, MTP) | 0 | Agent-Hirn. `-c 131072 --parallel 2` → **65536/Slot**, KV **q8_0**, `--spec-draft-n-max 3` |
|
| `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 |
|
||||||
| `embed` | Qwen3-Embedding-0.6B (Q8_0) | 0 | Mem0-Embeddings (1024 dim) |
|
| `coder` | Qwen3-Coder-Next (Q4_K_M) | 5400 | 131k ctx; Autonomer Coder & Builder (80B MoE, 51,5 t/s) |
|
||||||
| `reranker` | Qwen3-Reranker-0.6B (q8_0, **Mungert-GGUF**) | 0 | Gedächtnis-Sortierer (`/v1/rerank`); Community-GGUFs liefern oft Nullscores |
|
| `debugger` / `doctor` | Muse-Glimmer-30B (Q4_K_XL, DFlash) | 600 | 65k ctx; Runtime-Diagnostik & Fehler-Debugger (Multimodal) |
|
||||||
| `coder` | Qwen3-Coder-Next (Q4_K_M) | 600 | 131k ctx; Werkstatt-/Orchestrator-Worker + IDE „bauen"; auch `delegation.model` |
|
| `kritiker` | Devstral-Small-2-24B (Q4_K_M) | 1800 | 16k ctx Deckel; Fremdblick-Code-Reviewer gegen Halluzinationen |
|
||||||
| `heavy` | gpt-oss-120b (mxfp4) | 600 | 32k ctx! Planer/Chef-Gutachter/Radar-Analyst — nur Ein-Schuss, lädt seit der LLM-Diät in ~28 s |
|
| `vision` | Qwen3-VL-30B-A3B (Q4_K_M) | 900 | Multimodal-Augen (On-Demand) |
|
||||||
| `vision` | Qwen3-VL-30B-A3B (Q4_K_M) | 900 | Augen, ON-DEMAND; Lucy-App wärmt beim Start + alle 10 min |
|
| `scout` | GLM-4.6V-Flash (Q4_K_M) | 300 | Schneller Tool- und Vision-Allrounder |
|
||||||
| `kritiker` | GLM-4.7-Flash (UD-Q4_K_XL) | 600 | Fremdblick-Prosa + Chef-Gutachter-Vertretung |
|
| `heavy` | gpt-oss-120b (mxfp4) | 600 | 32k ctx; Chef-Gutachter (nur nachts / gezielter On-Demand Call) |
|
||||||
| `scout` | GLM-4.6V-Flash (Q4_K_M) | 300 | Vision+Tools-Allrounder |
|
| `dense-planer` | Qwen3.8-27B (Q4_K_M, BF16 mmproj) | 300 | 65k ctx; Dichtes 27B-Modell für tiefes Planen & Multimodalität |
|
||||||
|
| `reranker` | Qwen3-Reranker-0.6B (q8_0, Mungert) | 0 | Gedächtnis-Sortierer (`/v1/rerank`) |
|
||||||
|
| `embed` | Qwen3-Embedding-0.6B (f16) | 0 | Vektorisierung für Chroma / Mem0 |
|
||||||
|
|
||||||
Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben). Qwen3.5-122B und VL-8B sind
|
Config: `/etc/llama-swap/config.yaml` (sudo zum Schreiben).
|
||||||
ENTFERNT (07.07.) — die alten Rollback-Pfade existieren nicht mehr.
|
|
||||||
**Merkregeln:** Schutz-Key heißt `persistent:` (nicht `persist:`) · jedes brains-Mitglied
|
|
||||||
MUSS ttl:0 haben · nach JEDEM Config-Reload ist das Warm-Set leer → hermes/embed (+reranker)
|
|
||||||
per Mini-Request anwärmen (direkt curlen ist zuverlässiger als warmup.sh).
|
|
||||||
|
|
||||||
## Hermes (Agent)
|
## Hermes (Agent)
|
||||||
|
|
||||||
- **v0.18.2** (git-Install `~/.hermes/hermes-agent`, Py 3.11), Config `~/.hermes/config.yaml`
|
- **v0.20.4** (git-Install `~/.hermes/hermes-agent`), Config `~/.hermes/config.yaml`, Persona `~/.hermes/SOUL.md` = **Lucy**.
|
||||||
(IMMER Backup vor Änderung), Persona `~/.hermes/SOUL.md` = **Lucy** (alle Kanäle, Anrede
|
- **Bot Mode:** Unterstützt Multi-Bot-Roster (`Lucy` als Haupt-Agentin, `Coder` mit `Qwen3-Coder-Next`, `Debugger` mit `Muse-Glimmer-30B`).
|
||||||
„Commander").
|
- `approvals.mode: smart`, `approvals.cron_mode: deny`.
|
||||||
- Hirn: `model.default` UND `model.model` = `fast`. `delegation.model: coder` (seit 10.07.).
|
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-memory, mission-control-stack, hermes-pc-control, hermes-web-fetch, mcp_voice.
|
||||||
- `approvals.mode: smart`, `approvals.cron_mode: deny` · `platform_toolsets.api_server` =
|
|
||||||
Voice-Diät (NICHT mästen!) · cli-Bucket = volles Coding-Set (auch Hermes Desktop).
|
|
||||||
- MCP-Server (aus `~/mission-control-v2/mcp/`): mission-control-memory, mission-control-stack
|
|
||||||
(mcp_mc.py, 23 Tools), hermes-pc-control, hermes-web-fetch, mcp_voice (dünn, für Voice-Lane).
|
|
||||||
- **Hermes Desktop am PC:** lokales PC-Hermes mit Box-Gateway als Hirn (`:9001/v1`), Profil
|
|
||||||
`pc` für F:\-Arbeit, geteiltes Mem0 via mc2-memory-Plugin. Remote-Gateway der Box =
|
|
||||||
`http://192.168.178.151:9001/hermes-ui`.
|
|
||||||
|
|
||||||
## Automatik-Fahrplan (was nachts von allein läuft)
|
## Automatik-Fahrplan (was nachts von allein läuft)
|
||||||
|
|
||||||
@@ -82,14 +74,14 @@ per Mini-Request anwärmen (direkt curlen ist zuverlässiger als warmup.sh).
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 02:30 | PBS sichert alle Proxmox-LXCs |
|
| 02:30 | PBS sichert alle Proxmox-LXCs |
|
||||||
| 03:15 | **Traum-Cron** (dreaming-feed.sh → Wissens-Vault, fremdblick-Gate, propose-only) |
|
| 03:15 | **Traum-Cron** (dreaming-feed.sh → Wissens-Vault, fremdblick-Gate, propose-only) |
|
||||||
| 03:30 | Tarball-Backup (mem0+Configs+Secrets+`~/.hermes/cron`+state → `/srv/models/mc2-backups`, 14 Tage; rsync-Spiegel → Proxmox) |
|
| 03:30 | Tarball-Backup (Configs+Secrets+`~/.hermes/cron`+state → `/srv/models/mc2-backups`, 14 Tage; rsync-Spiegel → Proxmox) |
|
||||||
| 03:50 | PBS-Host-Backup der Box (`host/aibox`: mem0+hermes+vault+llamaswap.pxar) |
|
| 03:50 | PBS-Host-Backup der Box (`host/aibox`: hermes+vault+llamaswap.pxar) |
|
||||||
| 04:30 | **Chef-Gutachter** (gpt-oss richtet über die autonome Arbeit → Morgenlage) |
|
| 04:30 | **Chef-Gutachter** (gpt-oss richtet über die autonome Arbeit → Morgenlage) |
|
||||||
| 05:15 Mo | **Release-Radar** (hermes-Releases gegen die Eigenbau-Landkarte; erster Lauf 13.07.) |
|
| 05:15 Mo | **Release-Radar** (hermes-Releases gegen die Eigenbau-Landkarte; erster Lauf 13.07.) |
|
||||||
| 05:15 Sa | **Trend-Radar** (Live-Puls ALLER Modell-Rollen + Engine-A/B gegen neuen llama.cpp-Build + Watch-Liste [ROCm/MMQ/Community-Grid] + HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche; Frühwarnung vor dem So-Auto-Update) |
|
| 05:15 Sa | **Trend-Radar** (Live-Puls ALLER Modell-Rollen + Engine-A/B gegen neuen llama.cpp-Build + Watch-Liste [ROCm/MMQ/Community-Grid] + HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche; Frühwarnung vor dem So-Auto-Update) |
|
||||||
| 01:00 So | **Prüfstand** (volles Programm für Modell-Kandidaten: echte Coding-/Agenten-/Recherche-/Deutsch-/Vision-Prüfungen + Tempo auf :5899 gegen die Amtsinhaber-Baseline; Download-Deckel 35 GB, Cache räumt sich selbst, Ergebnis = Karte — Rollen-Wechsel bleibt Commander-Klick. Erkennt Engine-Drift → Tempo-Baseline-Refresh) |
|
| 01:00 So | **Prüfstand** (volles Programm für Modell-Kandidaten: echte Coding-/Agenten-/Recherche-/Deutsch-/Vision-Prüfungen + Tempo auf :5899 gegen die Amtsinhaber-Baseline; Download-Deckel 35 GB, Cache räumt sich selbst, Ergebnis = Karte — Rollen-Wechsel bleibt Commander-Klick. Erkennt Engine-Drift → Tempo-Baseline-Refresh) |
|
||||||
| 06:40, 1. d. M. | **Restore-Probe** (stellt WIRKLICH wieder her: PBS-Canary aus hermes.pxar via --pattern + Tarball-Probe + Frische-Check < 36 h; ⚠️-Alarm, wenn ein Backup nicht zurückkommt) |
|
| 06:40, 1. d. M. | **Restore-Probe** (stellt WIRKLICH wieder her: PBS-Canary aus hermes.pxar via --pattern + Tarball-Probe + Frische-Check < 36 h; ⚠️-Alarm, wenn ein Backup nicht zurückkommt) |
|
||||||
| 06:40, 2. d. M. | **Venv-Audit** (pip outdated + pip-audit-CVEs für mem0-/voice-venv; Funde = Karte, Update bleibt Commander-Entscheid) |
|
| 06:40, 2. d. M. | **Venv-Audit** (pip outdated + pip-audit-CVEs für voice-venv; Funde = Karte, Update bleibt Commander-Entscheid) |
|
||||||
| 07:00 | QNAP-Volume-Snapshot (7 behalten) |
|
| 07:00 | QNAP-Volume-Snapshot (7 behalten) |
|
||||||
| 07:15 | self-smoke (Gateway/Tools/Voice/:9119; Alarm nur bei Rot) |
|
| 07:15 | self-smoke (Gateway/Tools/Voice/:9119; Alarm nur bei Rot) |
|
||||||
| 08:00 | Daily-Briefing |
|
| 08:00 | Daily-Briefing |
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# ÜBERGABE: Drei-Welten-Umbau (19.07.2026) — letzter Claude-Stand
|
# ÜBERGABE: Drei-Welten-Umbau (19.07.2026) — letzter Claude-Stand
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> **UPDATE (August 2026):** Der hier beschriebene "Zed + OpenCode CLI"-Workflow wurde durch das "Zero Middle-Layers"-Prinzip ersetzt. Coding passiert jetzt zu 100% in der **OpenCode Desktop** Agentic IDE via MCP. Die technischen Entscheidungen zur Box und zu Gitea bleiben jedoch gültig.
|
||||||
**Dieses Dokument ist die finale Übergabe der externen Claude-Sessions an die Box.**
|
**Dieses Dokument ist die finale Übergabe der externen Claude-Sessions an die Box.**
|
||||||
Alles Wichtige aus dem Umbau vom 19.07. steht HIER (nicht nur im Claude-Gedächtnis,
|
Alles Wichtige aus dem Umbau vom 19.07. steht HIER (nicht nur im Claude-Gedächtnis,
|
||||||
das mit dem Abo-Ende unlesbar wird). Bei Widerspruch zu älteren Dokumenten gilt dieses.
|
das mit dem Abo-Ende unlesbar wird). Bei Widerspruch zu älteren Dokumenten gilt dieses.
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
|
|||||||
- **Agent-Runtime: Hermes bleibt.** ZeroClaw-PoC gebaut + abgerissen (01.07.) — der echte
|
- **Agent-Runtime: Hermes bleibt.** ZeroClaw-PoC gebaut + abgerissen (01.07.) — der echte
|
||||||
Latenz-Fix war das Hirn (gemma→Qwen3.6), nicht die Runtime. (ZeroClaw-Messwerte für den
|
Latenz-Fix war das Hirn (gemma→Qwen3.6), nicht die Runtime. (ZeroClaw-Messwerte für den
|
||||||
Fall „Hermes-Latenz wird wieder Thema": `docs/ZEROCLAW_POC_RESULTS.md`.)
|
Fall „Hermes-Latenz wird wieder Thema": `docs/ZEROCLAW_POC_RESULTS.md`.)
|
||||||
- **Memory-Layer: Mem0 bleibt.** Zep/Letta/Hindsight benchen höher, sind aber schwerer/teils
|
- **Memory-Layer: Mem0 ENTFERNT (07.08.2026).** Hermes-Agent ist die Single Source of Truth für Gedächtnis und Autonomie.
|
||||||
closed; Mem0 = beste Framework-Integration + v3-Algorithmus + läuft deployt.
|
|
||||||
- **Telegram bleibt DER Mobil-Kanal; WhatsApp VERWORFEN (09.07.).** Baileys-Bridge = Meta-ToS-
|
- **Telegram bleibt DER Mobil-Kanal; WhatsApp VERWORFEN (09.07.).** Baileys-Bridge = Meta-ToS-
|
||||||
Verstoß mit Bann-Risiko auf der Privatnummer + fragiler Dauerprozess; Alarmkette/Crons sind
|
Verstoß mit Bann-Risiko auf der Privatnummer + fragiler Dauerprozess; Alarmkette/Crons sind
|
||||||
simple Bot-API-Calls. Neu bewerten nur bei offizieller Personal-Bot-API.
|
simple Bot-API-Calls. Neu bewerten nur bei offizieller Personal-Bot-API.
|
||||||
@@ -41,7 +40,7 @@ einen User-Entscheid — kein „Best Practice sagt aber…" (siehe ARBEITSWEISE
|
|||||||
89–94. Draft: `giocom-…-DFlash-Q8_0.gguf` (Mainline-kompatibel; abhinand-Konvertierung ist es
|
89–94. Draft: `giocom-…-DFlash-Q8_0.gguf` (Mainline-kompatibel; abhinand-Konvertierung ist es
|
||||||
NICHT — fehlendes `target_layers`-Metadatum). Rollback-Backup `config.yaml.bak-dflash-*`.
|
NICHT — fehlendes `target_layers`-Metadatum). Rollback-Backup `config.yaml.bak-dflash-*`.
|
||||||
KV **q8_0** (Entscheid 07.07.: Qualität > ein paar GB; der q4_k-Commit lief nie live).
|
KV **q8_0** (Entscheid 07.07.: Qualität > ein paar GB; der q4_k-Commit lief nie live).
|
||||||
`--parallel 2` = bewusster Trade (Mem0-Lernen blockiert nie den nächsten Voice-Turn).
|
`--parallel 2` = bewusster Trade (Lernen blockiert nie den nächsten Voice-Turn).
|
||||||
- **DFlash je Rolle (gemessen 22.07., nicht neu aufrollen):** coder NEIN — 44–50 t/s mit Draft
|
- **DFlash je Rolle (gemessen 22.07., nicht neu aufrollen):** coder NEIN — 44–50 t/s mit Draft
|
||||||
(Q8 wie BF16) vs. 59–60 ohne, trotz 68–76 % Akzeptanz: 3B-aktiv-Decode ist billiger als das
|
(Q8 wie BF16) vs. 59–60 ohne, trotz 68–76 % Akzeptanz: 3B-aktiv-Decode ist billiger als das
|
||||||
Draften. heavy WARTET — gpt-oss-120b-Draft konvertiert (liegt in `/srv/models/DFlash-drafts/`),
|
Draften. heavy WARTET — gpt-oss-120b-Draft konvertiert (liegt in `/srv/models/DFlash-drafts/`),
|
||||||
|
|||||||
+2
-7
File diff suppressed because one or more lines are too long
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+11
-6
File diff suppressed because one or more lines are too long
Vendored
+14
-4
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-44
File diff suppressed because one or more lines are too long
-31
File diff suppressed because one or more lines are too long
+36
File diff suppressed because one or more lines are too long
-30
File diff suppressed because one or more lines are too long
+35
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
|||||||
|
import{u,ah as m,k as x,j as s,y as n,ai as b,b as p,n as f,q as h}from"./index-0Xcu6PnS.js";function y(){const d=u(),{data:t=[]}=m(),{showAlert:l,dialogElement:o}=x();async function i(e){try{await f(`/api/jobs/${e}/cancel`,{method:"POST"}),d.invalidateQueries({queryKey:h.jobs})}catch(c){l("Fehler",c.message)}}const a=t.filter(e=>e.state==="running"||e.state==="queued"),r=t.filter(e=>e.state!=="running"&&e.state!=="queued").slice(-3);return a.length===0&&r.length===0?null:s.jsxs("div",{className:"space-y-3 mc-card p-4",children:[s.jsx("div",{className:"text-[11px] font-semibold text-muted-foreground uppercase tracking-wider",children:"Aktive Downloads"}),a.map(e=>s.jsxs("div",{className:"space-y-1.5 p-3 rounded-xl bg-background/20 border border-border/40",children:[s.jsxs("div",{className:"flex justify-between items-center text-xs",children:[s.jsx("span",{className:"font-semibold truncate max-w-[250px]",children:e.label}),s.jsxs("div",{className:"flex items-center gap-3",children:[s.jsxs("span",{className:"text-muted-foreground font-mono",children:[e.progress??0,"% • ",n(e.done_bytes),"/",n(e.total_bytes),e.eta_s?` • ETA ${b(e.eta_s)}`:""]}),s.jsx("button",{onClick:()=>i(e.id),className:"text-[10px] text-red-400 hover:text-red-300 font-semibold border border-red-500/25 bg-red-500/5 px-2 py-0.5 rounded transition-all cursor-pointer",children:"Abbrechen"})]})]}),s.jsx("div",{className:"h-1.5 overflow-hidden rounded-full bg-muted",children:s.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-500",style:{width:`${e.progress??0}%`}})})]},e.id)),r.map(e=>s.jsxs("div",{className:"flex justify-between items-center text-xs text-muted-foreground px-1",children:[s.jsx("span",{className:"truncate",children:e.label}),s.jsx("span",{className:p("font-semibold text-[10px] px-1.5 py-0.5 rounded uppercase font-mono",e.state==="done"?"bg-emerald-500/10 text-emerald-400":"bg-amber-500/10 text-amber-400"),children:e.state})]},e.id)),o]})}export{y as J};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{I as b,u as f,K as h,r as d,j as e,b as c,J as g,T as p,R as j,Q as N,n as v,U as w,q as x}from"./index-0Xcu6PnS.js";function y(){const{data:s}=b(),m=f(),r=s!=null&&s.box_console_url?h(s.box_console_url):void 0,a=s==null?void 0:s.box_console_reachable,[l,o]=d.useState(!1),[i,n]=d.useState("");async function u(){o(!0),n("");try{const t=await v("/api/maintenance/restart",{method:"POST",body:JSON.stringify({service:"box-console"})});n(t.ok?"Neu gestartet — einen Moment, dann lädt das Terminal.":`Fehlgeschlagen: ${t.err||"Unbekannter Fehler"}`),w(m,x.agentStatus,x.services)}catch(t){n(`Fehlgeschlagen: ${(t==null?void 0:t.message)||t}`)}finally{o(!1)}}return e.jsxs("div",{className:"flex h-full flex-col gap-4",children:[e.jsxs("div",{className:"flex flex-wrap items-end justify-between gap-3",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent",children:"Konsole"}),e.jsx("p",{className:"flex items-center gap-2 text-sm text-muted-foreground",children:"Direkte Shell auf der Box — wie ein SSH-Fenster, mitten im Browser."})]}),e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("span",{className:"flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground",children:[e.jsx("span",{className:c("h-2 w-2 rounded-full",a?"bg-emerald-500 animate-pulse":"bg-amber-500")}),a?"online":"offline"]}),r&&e.jsxs("a",{href:r,target:"_blank",rel:"noopener",className:"flex h-8 items-center gap-1.5 rounded-lg border border-border/60 bg-background/20 px-3 text-xs font-semibold text-muted-foreground transition-all hover:border-primary/50 hover:text-foreground",children:[e.jsx(g,{className:"h-3.5 w-3.5"})," In neuem Tab"]})]})]}),r?e.jsxs("div",{className:"relative min-h-[58vh] flex-1 overflow-hidden rounded-2xl border border-border/60 bg-black/50 shadow-lg shadow-black/25",children:[a===!1&&e.jsxs("div",{className:"absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-black/70 text-center",children:[e.jsx(p,{className:"h-8 w-8 text-amber-400"}),e.jsx("div",{className:"text-sm font-semibold text-amber-300",children:"Konsole nicht erreichbar"}),e.jsxs("p",{className:"max-w-sm text-[11px] leading-normal text-muted-foreground",children:["Der Terminal-Dienst (",e.jsx("code",{className:"font-mono text-primary",children:"box-console"}),") läuft gerade nicht."]}),e.jsxs("button",{onClick:u,disabled:l,className:"flex h-9 items-center gap-1.5 rounded-lg border border-amber-500/40 bg-amber-500/10 px-4 text-[11px] font-bold uppercase tracking-wide text-amber-300 transition-all hover:bg-amber-500/20 cursor-pointer disabled:opacity-50",children:[e.jsx(j,{className:c("h-3.5 w-3.5",l&&"animate-spin")})," Dienst neu starten"]}),i&&e.jsx("p",{className:"max-w-sm text-[11px] text-muted-foreground",children:i})]}),e.jsx("iframe",{src:r,title:"Box-Konsole",className:"h-full w-full border-0",style:{minHeight:"58vh"}})]}):e.jsxs("div",{className:"flex min-h-[58vh] flex-1 items-center justify-center rounded-2xl border border-border/60 bg-background/20 text-xs text-muted-foreground",children:[e.jsx(N,{className:"mr-2 h-4 w-4"})," Lade Konsole…"]})]})}export{y as KonsoleView};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{W as b,u as f,Z as h,r as d,j as e,e as c,Y as g,T as p,a1 as j,a2 as N,g as v,a3 as w,q as x}from"./index-Cejiqiju.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};
|
|
||||||
-52
File diff suppressed because one or more lines are too long
+83
File diff suppressed because one or more lines are too long
-73
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as s,j as e}from"./index-Cejiqiju.js";/**
|
import{c as s,j as e}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
+6
@@ -0,0 +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-0Xcu6PnS.js";import{J as _}from"./JobsBar-JgL4-cSI.js";var c,g,o,h,m,w,C,q,z=(q=class extends B{constructor(e,t){super();y(this,m);y(this,c);y(this,g);y(this,o);y(this,h);v(this,c,e),this.setOptions(t),this.bindMethods(),j(this,m,w).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var n;const t=this.options;this.options=a(this,c).defaultMutationOptions(e),F(this.options,t)||a(this,c).getMutationCache().notify({type:"observerOptionsUpdated",mutation:a(this,o),observer:this}),t!=null&&t.mutationKey&&this.options.mutationKey&&E(t.mutationKey)!==E(this.options.mutationKey)?this.reset():((n=a(this,o))==null?void 0:n.state.status)==="pending"&&a(this,o).setOptions(this.options)}onUnsubscribe(){var e;this.hasListeners()||(e=a(this,o))==null||e.removeObserver(this)}onMutationUpdate(e){j(this,m,w).call(this),j(this,m,C).call(this,e)}getCurrentResult(){return a(this,g)}reset(){var e;(e=a(this,o))==null||e.removeObserver(this),v(this,o,void 0),j(this,m,w).call(this),j(this,m,C).call(this)}mutate(e,t){var n;return v(this,h,t),(n=a(this,o))==null||n.removeObserver(this),v(this,o,a(this,c).getMutationCache().build(a(this,c),this.options)),a(this,o).addObserver(this),a(this,o).execute(e)}},c=new WeakMap,g=new WeakMap,o=new WeakMap,h=new WeakMap,m=new WeakSet,w=function(){var t;const e=((t=a(this,o))==null?void 0:t.state)??I();v(this,g,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},C=function(e){P.batch(()=>{var t,n,l,x,d,p,k,i;if(a(this,h)&&this.hasListeners()){const u=a(this,g).variables,N=a(this,g).context,S={client:a(this,c),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(n=(t=a(this,h)).onSuccess)==null||n.call(t,e.data,u,N,S)}catch(b){Promise.reject(b)}try{(x=(l=a(this,h)).onSettled)==null||x.call(l,e.data,null,u,N,S)}catch(b){Promise.reject(b)}}else if((e==null?void 0:e.type)==="error"){try{(p=(d=a(this,h)).onError)==null||p.call(d,e.error,u,N,S)}catch(b){Promise.reject(b)}try{(i=(k=a(this,h)).onSettled)==null||i.call(k,void 0,e.error,u,N,S)}catch(b){Promise.reject(b)}}}this.listeners.forEach(u=>{u(a(this,g))})})},q);function W(r,e){const t=A(),[n]=f.useState(()=>new z(t,r));f.useEffect(()=>{n.setOptions(r)},[n,r]);const l=f.useSyncExternalStore(f.useCallback(d=>n.subscribe(P.batchCalls(d)),[n]),()=>n.getCurrentResult(),()=>n.getCurrentResult()),x=f.useCallback((d,p)=>{n.mutate(d,p).catch(J)},[n]);if(l.error&&Q(n.options.throwOnError,[l.error]))throw l.error;return{...l,mutate:x,mutateAsync:l.mutate}}/**
|
||||||
|
* @license lucide-react v0.460.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* 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};
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
import{ao as y,r as x,g as L,j as e,L as v,S,af as W,R as C,e as w,ap as E}from"./index-Cejiqiju.js";import{F as M}from"./folder-open-B5JYx4j5.js";const D={"":"Allgemein",traeume:"Träume",muster:"Muster","skill-kandidaten":"Skill-Kandidaten","skill-kandidaten/beauftragt":"Skill-Kandidaten · beauftragt","skill-kandidaten/verworfen":"Skill-Kandidaten · verworfen"};function R(){const{data:o,isLoading:h}=y(),[l,f]=x.useState(null),[d,r]=x.useState(null),[u,i]=x.useState(!1),[a,p]=x.useState(""),t=x.useMemo(()=>(o==null?void 0:o.files)??[],[o]),m=x.useMemo(()=>{const s=new Map;for(const n of t)s.set(n.name.toLowerCase(),n.path);return s},[t]),g=x.useMemo(()=>{if(!a.trim())return t;const s=a.toLowerCase();return t.filter(n=>n.title.toLowerCase().includes(s)||n.path.toLowerCase().includes(s))},[t,a]),b=x.useMemo(()=>{const s=[];for(const n of g){const c=s.find(k=>k.dir===n.dir);c?c.files.push(n):s.push({dir:n.dir,files:[n]})}return s.sort((n,c)=>n.dir===""?-1:c.dir===""?1:n.dir.localeCompare(c.dir))},[g]);x.useEffect(()=>{if(!l&&t.length){const s=t.find(n=>n.path.toLowerCase()==="index.md");f((s==null?void 0:s.path)??t[0].path)}},[t,l]),x.useEffect(()=>{l&&(i(!0),L(`/api/wissen/datei?pfad=${encodeURIComponent(l)}`).then(r).catch(()=>r({path:l,content:"*(Notiz nicht ladbar)*",mtime:0})).finally(()=>i(!1)))},[l]);const j=s=>{const n=m.get(s.toLowerCase());n&&f(n)};return e.jsxs("div",{className:"space-y-5",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:"Wissen"}),e.jsx("p",{className:"text-sm text-muted-foreground",children:"Was sich die Box nachts erschließt — Traum-Notizen, Muster und Skill-Ideen aus dem Wissens-Vault, verlinkt wie ein Wiki."})]}),o&&!o.available&&e.jsx("div",{className:"rounded-2xl border border-amber-500/30 bg-amber-500/5 p-4 text-xs text-amber-300",children:"Der Wissens-Vault liegt auf der Box (~/wissens-vault) — im lokalen Dev-Modus nicht verfügbar."}),h&&e.jsxs("div",{className:"flex items-center gap-2 rounded-2xl border border-border/60 bg-card/30 p-6 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Vault wird geladen …"]}),(o==null?void 0:o.available)&&t.length===0&&e.jsx("div",{className:"rounded-2xl border border-dashed border-border/60 bg-card/20 p-8 text-center text-xs text-muted-foreground",children:"Noch keine Notizen. Der nächtliche Traum (03:15) legt sie an — morgen früh steht hier die erste."}),t.length>0&&e.jsxs("div",{className:"flex flex-col gap-4 lg:flex-row",children:[e.jsxs("aside",{className:"w-full shrink-0 space-y-3 lg:w-72",children:[e.jsxs("div",{className:"relative",children:[e.jsx("input",{value:a,onChange:s=>p(s.target.value),type:"search",placeholder:"Notizen durchsuchen…","aria-label":"Notizen durchsuchen",className:"h-9 w-full rounded-lg border border-border/60 bg-card/45 pl-8 pr-3 text-xs text-foreground outline-none focus:ring-1 focus:ring-primary/50"}),e.jsx(S,{className:"absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground"})]}),e.jsx("div",{className:"max-h-[70vh] space-y-3 overflow-y-auto pr-1 scrollbar-thin",children:b.map(({dir:s,files:n})=>e.jsxs("div",{children:[e.jsxs("p",{className:"mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground/60",children:[e.jsx(M,{className:"h-3 w-3"})," ",D[s]??s]}),e.jsx("div",{className:"space-y-0.5",children:n.map(c=>e.jsxs("button",{onClick:()=>f(c.path),className:w("flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-left text-xs transition-all cursor-pointer",l===c.path?"bg-primary/15 text-primary":"text-muted-foreground hover:bg-accent hover:text-foreground"),children:[e.jsx(W,{className:"h-3.5 w-3.5 shrink-0"}),e.jsx("span",{className:"truncate",title:c.title,children:c.name}),c.neu&&e.jsxs("span",{className:"ml-auto flex items-center gap-0.5 rounded bg-violet-500/15 px-1 py-0.5 text-[8px] font-bold uppercase text-violet-300",children:[e.jsx(C,{className:"h-2.5 w-2.5"}),"neu"]})]},c.path))})]},s||"__root"))})]}),e.jsx("div",{className:"min-w-0 flex-1 mc-card p-5",children:u?e.jsxs("div",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:[e.jsx(v,{className:"h-4 w-4 animate-spin"})," Notiz wird geladen …"]}):d?e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"mb-3 flex items-center justify-between border-b border-border/40 pb-2",children:[e.jsxs("span",{className:"flex items-center gap-1.5 font-mono text-[11px] text-muted-foreground",children:[e.jsx(E,{className:"h-3.5 w-3.5"})," ",d.path]}),d.mtime>0&&e.jsxs("span",{className:"text-[10px] text-muted-foreground/60",children:["Stand ",new Date(d.mtime*1e3).toLocaleString("de-DE",{day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"})]})]}),e.jsx(z,{text:d.content,onWikiLink:j,known:m})]}):null})]})]})}function z({text:o,onWikiLink:h,known:l}){const f=o.split(`
|
|
||||||
`),d=[];let r=[],u=null;const i=a=>{r.length&&(d.push(e.jsx("ul",{className:"mb-3 ml-4 list-disc space-y-1",children:r},a)),r=[])};return f.forEach((a,p)=>{const t=`l${p}`;if(u!==null){a.trimEnd()==="```"?(d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px] leading-relaxed text-foreground/85",children:u.join(`
|
|
||||||
`)},t)),u=null):u.push(a);return}if(a.trimStart().startsWith("```")){i(t),u=[];return}const m=a.trimEnd();if(!m.trim()){i(t);return}const g=m.match(/^(#{1,4})\s+(.*)$/);if(g){i(t);const j=g[1].length,s=j===1?"text-lg font-bold mt-1 mb-3":j===2?"text-base font-bold mt-4 mb-2":"text-sm font-bold mt-3 mb-1.5";d.push(e.jsx("p",{className:w(s,"font-space text-foreground"),children:N(g[2],h,l,t)},t));return}const b=m.match(/^\s*[-*•]\s+(.*)$/);if(b){r.push(e.jsx("li",{className:"text-xs leading-relaxed text-foreground/85",children:N(b[1],h,l,t)},t));return}i(t),d.push(e.jsx("p",{className:"mb-2 text-xs leading-relaxed text-foreground/85",children:N(m,h,l,t)},t))}),i("end"),u!==null&&d.push(e.jsx("pre",{className:"mb-3 overflow-x-auto rounded-xl border border-border/50 bg-background/50 p-3 font-mono text-[11px]",children:u.join(`
|
|
||||||
`)},"code-end")),e.jsx("div",{className:"max-w-3xl",children:d})}function N(o,h,l,f){return o.split(/(\[\[[^\]]+\]\]|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g).map((r,u)=>{const i=`${f}-${u}`,a=r.match(/^\[\[([^\]|]+)(?:\|([^\]]+))?\]\]$/);if(a){const p=a[1].trim(),t=(a[2]??a[1]).trim();return l.has(p.toLowerCase())?e.jsx("button",{onClick:()=>h(p),className:"rounded bg-primary/10 px-1 text-primary underline decoration-primary/40 underline-offset-2 hover:bg-primary/20 cursor-pointer",children:t},i):e.jsx("span",{className:"rounded bg-background/40 px-1 text-muted-foreground",title:"Notiz existiert (noch) nicht",children:t},i)}return r.startsWith("**")&&r.endsWith("**")?e.jsx("b",{className:"font-semibold text-foreground",children:r.slice(2,-2)},i):r.startsWith("*")&&r.endsWith("*")&&r.length>2?e.jsx("i",{children:r.slice(1,-1)},i):r.startsWith("`")&&r.endsWith("`")?e.jsx("code",{className:"rounded bg-background/50 px-1 font-mono text-[11px] text-teal-300",children:r.slice(1,-1)},i):e.jsx("span",{children:r},i)})}export{R as WissenView};
|
|
||||||
+47
File diff suppressed because one or more lines are too long
-6
@@ -1,6 +0,0 @@
|
|||||||
import{c as a}from"./index-Cejiqiju.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("BookOpen",[["path",{d:"M12 7v14",key:"1akyts"}],["path",{d:"M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z",key:"ruj8y"}]]);export{e as B};
|
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as o}from"./index-Cejiqiju.js";/**
|
import{c as o}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
import{c}from"./index-Cejiqiju.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=c("CircleX",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m15 9-6 6",key:"1uzhvr"}],["path",{d:"m9 9 6 6",key:"z0biqf"}]]);export{r as C};
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-Cejiqiju.js";/**
|
import{c as e}from"./index-0Xcu6PnS.js";/**
|
||||||
* @license lucide-react v0.460.0 - ISC
|
* @license lucide-react v0.460.0 - ISC
|
||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user