Governor Phase 0 fertig + Phase 2: Hart-Deckel default + Sprach-Signal an Lucy
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>
This commit is contained in:
@@ -39,18 +39,27 @@ Alle anderen Pfade (`/v1/models` etc.) werden roh durchgereicht. Streaming (SSE)
|
|||||||
byteweise durchgereicht; die echten `prompt_tokens` aus der Antwort werden zur Kalibrierung
|
byteweise durchgereicht; die echten `prompt_tokens` aus der Antwort werden zur Kalibrierung
|
||||||
mitgeloggt.
|
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
|
### Umgebungsvariablen
|
||||||
`GOV_PORT` (8100) · `GOV_HOST` (0.0.0.0) · `GOV_UPSTREAM` (http://127.0.0.1:8080) ·
|
`GOV_PORT` (8100) · `GOV_HOST` (0.0.0.0) · `GOV_UPSTREAM` (http://127.0.0.1:8080) ·
|
||||||
`GOV_THRESHOLD` (25000) · `GOV_HARD_CEILING` (0=aus) · `GOV_CHARS_PER_TOKEN` (3.5) ·
|
`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_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)
|
## Auf der Box laufen lassen (wie in P0 aufgesetzt)
|
||||||
```bash
|
```bash
|
||||||
# Aider (einmalig, unter isoliertem Python 3.12 — System-Python 3.14 bricht Aiders Pins):
|
# 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
|
pipx install uv && uv tool install --python 3.12 aider-chat
|
||||||
|
|
||||||
# Dateien liegen in ~/governor-p0/. Governor starten (Soft 25k, Hart aus):
|
# Dateien liegen in ~/governor-p0/. Governor starten (Hart-Deckel default AN = Soft+5000):
|
||||||
~/governor-p0/gov-ctl.sh start 25000 0 # oder: start 25000 30000 (Hart an)
|
~/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:
|
# Aider-Sitzung durch den Governor:
|
||||||
~/governor-p0/run-driver.sh ~/governor-p0/msgs-todo.txt
|
~/governor-p0/run-driver.sh ~/governor-p0/msgs-todo.txt
|
||||||
@@ -91,15 +100,16 @@ verweigert das Modell zwar den Code, schreibt die Absichten aber fortschreitend
|
|||||||
in SAVEPOINT — genährt von Aiders **irreführenden Commit-Nachrichten** (die aus dem SAVEPOINT-
|
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
|
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
|
nicht gab). Deshalb der optionale **Hart-Deckel** (`GOV_HARD_CEILING`): oberhalb davon antwortet
|
||||||
der Governor selbst, das Modell kann keine degradierenden Savepoints mehr schreiben. **Empfehlung:
|
der Governor selbst, das Modell kann keine degradierenden Savepoints mehr schreiben. **Der Hart-
|
||||||
Hart-Deckel aktiv** (z. B. `Soft 25000, Hart 30000` — ein Finalisier-Zug Luft, dann harter Riegel).
|
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
|
### Weitere Befunde / Fallen
|
||||||
- **Aiders eigene Zusammenfassung MUSS aus** (`summarizer.max_tokens` hoch) — sonst compactet
|
- **Aiders eigene Zusammenfassung MUSS aus** (`summarizer.max_tokens` hoch) — sonst compactet
|
||||||
Aider selbst und der Governor greift nie. Siehe oben.
|
Aider selbst und der Governor greift nie. Siehe oben.
|
||||||
- **Commit-Nachrichten überzeichnen** in der Abschluss-Phase (aus SAVEPOINT-Absicht). Der Code
|
- **Commit-Nachrichten überzeichnen** in der Abschluss-Phase (aus SAVEPOINT-Absicht). Der Code
|
||||||
ist die Wahrheit; git-Nachrichten sind es hier nicht. → Für Phase 1: Auto-Commit nahe/nach dem
|
ist die Wahrheit; git-Nachrichten sind es hier nicht. Der Hart-Deckel (jetzt Default) begrenzt
|
||||||
Feuern zügeln oder Savepoint mit ehrlicher fixer Nachricht committen.
|
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.14 auf der Box bricht Aiders Pins** (numpy 1.24.3) → Aider via `uv` unter isoliertem
|
||||||
Python 3.12 installiert.
|
Python 3.12 installiert.
|
||||||
- **Aider-Scripting-API (`coder.run`) hängt** in einer Datei-Hinzufügen-Reflexion (Edits landeten
|
- **Aider-Scripting-API (`coder.run`) hängt** in einer Datei-Hinzufügen-Reflexion (Edits landeten
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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")'
|
||||||
@@ -13,18 +13,20 @@ OUT="$DIR/governor.stdout"
|
|||||||
start() {
|
start() {
|
||||||
stop
|
stop
|
||||||
local thr="${1:-25000}"
|
local thr="${1:-25000}"
|
||||||
local hard="${2:-${GOV_HARD_CEILING:-0}}"
|
|
||||||
cd "$DIR"
|
cd "$DIR"
|
||||||
: > "$LOG"
|
: > "$LOG"
|
||||||
# Voll detachen: eigene Session, alle FDs weg vom Aufrufer.
|
# Voll detachen: eigene Session, alle FDs weg vom Aufrufer.
|
||||||
# CPT (Zeichen/Token) aus der Umgebung durchreichen, Default 3.5 (kalibriert 24.07.).
|
# Hart-Deckel nur setzen, wenn explizit uebergeben (Arg 2); sonst rechnet
|
||||||
setsid env GOV_THRESHOLD="$thr" GOV_HARD_CEILING="$hard" \
|
# 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_CHARS_PER_TOKEN="${GOV_CHARS_PER_TOKEN:-3.5}" \
|
||||||
GOV_LOG="$LOG" GOV_PORT=8100 \
|
GOV_LOG="$LOG" GOV_PORT=8100 \
|
||||||
python3 "$DIR/governor.py" </dev/null >>"$OUT" 2>&1 &
|
python3 "$DIR/governor.py" </dev/null >>"$OUT" 2>&1 &
|
||||||
echo $! > "$PIDF"
|
echo $! > "$PIDF"
|
||||||
sleep 1.2
|
sleep 1.2
|
||||||
echo "started pid=$(cat "$PIDF") threshold=$thr hard=$hard"
|
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"
|
ss -tlnp 2>/dev/null | grep ":8100" >/dev/null && echo "listening :8100 OK" || echo "WARN: not listening"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ Konfiguration per Umgebungsvariablen (alle optional):
|
|||||||
GOV_HOST Listen-Adresse (Default 0.0.0.0)
|
GOV_HOST Listen-Adresse (Default 0.0.0.0)
|
||||||
GOV_UPSTREAM Modell-Endpoint (Default http://127.0.0.1:8080)
|
GOV_UPSTREAM Modell-Endpoint (Default http://127.0.0.1:8080)
|
||||||
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 25000)
|
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 25000)
|
||||||
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default 0)
|
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default: Soft+5000, AN)
|
||||||
GOV_CHARS_PER_TOKEN Heuristik Zeichen->Token (Default 3.5, kalibriert)
|
GOV_CHARS_PER_TOKEN Heuristik Zeichen->Token (Default 3.5, kalibriert)
|
||||||
GOV_LOG Logdatei (zusaetzlich zu stdout) (Default ./governor.log)
|
GOV_LOG Logdatei (zusaetzlich zu stdout) (Default ./governor.log)
|
||||||
GOV_DIRECTIVE Text des Soft-Einschubs (sonst Default unten)
|
GOV_DIRECTIVE Text des Soft-Einschubs (sonst Default unten)
|
||||||
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort (sonst Default unten)
|
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort (sonst Default unten)
|
||||||
|
GOV_ANNOUNCE_URL Lucy-Sprach-Signal-Endpunkt; "" = aus (Default :9001/api/voice/announce)
|
||||||
|
GOV_ANNOUNCE_THROTTLE Sekunden zwischen Signalen (Default 300)
|
||||||
|
GOV_ANNOUNCE_TEXT Text des Sprach-Signals (sonst Default)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import http.client
|
import http.client
|
||||||
@@ -43,7 +46,11 @@ PORT = int(os.environ.get("GOV_PORT", "8100"))
|
|||||||
HOST = os.environ.get("GOV_HOST", "0.0.0.0")
|
HOST = os.environ.get("GOV_HOST", "0.0.0.0")
|
||||||
UPSTREAM = os.environ.get("GOV_UPSTREAM", "http://127.0.0.1:8080")
|
UPSTREAM = os.environ.get("GOV_UPSTREAM", "http://127.0.0.1:8080")
|
||||||
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "25000"))
|
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "25000"))
|
||||||
HARD_CEILING = int(os.environ.get("GOV_HARD_CEILING", "0")) # 0 = deaktiviert
|
# Hart-Deckel: Default AN (Soft + 5000 = ein Finalisier-Zug Luft), weil der weiche
|
||||||
|
# Schnitt allein bei Weiterarbeit ueber die Grenze eine Fassade erzeugt (Befund P0).
|
||||||
|
# Explizit setzbar; GOV_HARD_CEILING=0 schaltet ihn aus.
|
||||||
|
_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.5"))
|
CHARS_PER_TOKEN = float(os.environ.get("GOV_CHARS_PER_TOKEN", "3.5"))
|
||||||
LOG_PATH = os.environ.get("GOV_LOG", os.path.join(os.getcwd(), "governor.log"))
|
LOG_PATH = os.environ.get("GOV_LOG", os.path.join(os.getcwd(), "governor.log"))
|
||||||
|
|
||||||
@@ -69,6 +76,18 @@ DEFAULT_HARDSTOP = (
|
|||||||
)
|
)
|
||||||
HARDSTOP_MSG = os.environ.get("GOV_HARDSTOP_MSG", DEFAULT_HARDSTOP)
|
HARDSTOP_MSG = os.environ.get("GOV_HARDSTOP_MSG", DEFAULT_HARDSTOP)
|
||||||
|
|
||||||
|
# Sprach-Signal an Lucy (Phase 2): beim Feuern POSTet der Governor eine Meldung an die
|
||||||
|
# vorhandene MC2-Announce-Pipeline (:9001). Lucy pollt sie ohnehin, dedupliziert und
|
||||||
|
# spricht sie (gated durch ihren "Box-Meldungen laut"-Schalter). Best-effort, gedrosselt
|
||||||
|
# gegen die Pro-Runde-Feuerung. GOV_ANNOUNCE_URL="" schaltet das Signal ab.
|
||||||
|
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")) # Sekunden
|
||||||
|
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 = urlparse(UPSTREAM)
|
||||||
UP_HOST = up.hostname or "127.0.0.1"
|
UP_HOST = up.hostname or "127.0.0.1"
|
||||||
UP_PORT = up.port or 80
|
UP_PORT = up.port or 80
|
||||||
@@ -80,6 +99,9 @@ HOP_BY_HOP = {
|
|||||||
|
|
||||||
_log_lock = threading.Lock()
|
_log_lock = threading.Lock()
|
||||||
_PROMPT_TOKENS_RE = re.compile(r'"prompt_tokens"\s*:\s*(\d+)')
|
_PROMPT_TOKENS_RE = re.compile(r'"prompt_tokens"\s*:\s*(\d+)')
|
||||||
|
_announce_lock = threading.Lock()
|
||||||
|
_last_announce = 0.0 # Zeitstempel der letzten Meldung (Drossel)
|
||||||
|
_an = urlparse(ANNOUNCE_URL) if ANNOUNCE_URL else None
|
||||||
|
|
||||||
|
|
||||||
def log(line: str) -> None:
|
def log(line: str) -> None:
|
||||||
@@ -115,6 +137,40 @@ def estimate_tokens(messages) -> int:
|
|||||||
return int(chars / CHARS_PER_TOKEN)
|
return int(chars / CHARS_PER_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_announce(est) -> None:
|
||||||
|
"""POSTet die Meldung an die MC2-Announce-Pipeline. Laeuft im Hintergrund-Thread."""
|
||||||
|
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: # noqa: BLE001
|
||||||
|
log(f"ANNOUNCE fehlgeschlagen: {exc!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_announce(est) -> None:
|
||||||
|
"""Sprach-Signal an Lucy ausloesen — gedrosselt, damit die Pro-Runde-Feuerung
|
||||||
|
nicht spammt (eine Aeusserung je Ueberschreitungs-Episode). Best-effort."""
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
protocol_version = "HTTP/1.1"
|
||||||
server_version = "Governor/0.2"
|
server_version = "Governor/0.2"
|
||||||
@@ -161,6 +217,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
model = ""
|
model = ""
|
||||||
if is_chat and body:
|
if is_chat and body:
|
||||||
action, body, est, streaming, model = self._decide(body)
|
action, body, est, streaming, model = self._decide(body)
|
||||||
|
if action in ("soft", "hard"):
|
||||||
|
maybe_announce(est) # Sprach-Signal an Lucy (gedrosselt)
|
||||||
|
|
||||||
# HARTER STOPP: selbst antworten, Upstream nie fragen.
|
# HARTER STOPP: selbst antworten, Upstream nie fragen.
|
||||||
if action == "hard":
|
if action == "hard":
|
||||||
|
|||||||
Reference in New Issue
Block a user