798de0be8f
Phase 0 abgeschlossen: - Hart-Deckel jetzt Default AN (Soft+5000), da der weiche Schnitt allein bei Weiterarbeit ueber die Grenze eine Fassade erzeugt (P0-Befund). GOV_HARD_CEILING=0 schaltet ihn aus. gov-ctl reicht Arg 2 nur bei Bedarf durch. - README: Empfehlung/Doku auf Default-an aktualisiert, Commit-Msg-Restpunkt notiert (--no-auto-commits als saubere Option). Phase 2 (Voice-Hook): beim Feuern (soft/hard) POSTet der Governor best-effort + gedrosselt (Default 300 s) eine Meldung an Lucys vorhandene Announce-Pipeline (POST :9001/api/voice/announce, source=governor, priority=normal). Lucy pollt, dedupliziert und spricht sie via lokales TTS -- KEIN Lucy-Code noetig. E2E bewiesen: Feuern -> ANNOUNCE status=200 -> Eintrag id 394 source=governor in der Queue. GOV_ANNOUNCE_URL="" schaltet es ab. announce-test.sh beigelegt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
8.6 KiB
Markdown
135 lines
8.6 KiB
Markdown
# 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.
|