Compare commits
2 Commits
775e862652
...
59d6be8c4a
| Author | SHA1 | Date | |
|---|---|---|---|
| 59d6be8c4a | |||
| 7fac17ed9a |
@@ -11,6 +11,10 @@
|
|||||||
# sonst bricht `#!/usr/bin/env python3\r`. (Nur die Hooks + Deploy-Helfer, nicht der ganze Baum.)
|
# sonst bricht `#!/usr/bin/env python3\r`. (Nur die Hooks + Deploy-Helfer, nicht der ganze Baum.)
|
||||||
deploy/agent-hooks/*.py text eol=lf
|
deploy/agent-hooks/*.py text eol=lf
|
||||||
deploy/*.py text eol=lf
|
deploy/*.py text eol=lf
|
||||||
|
# `deploy/*.py` greift NUR eine Ebene tief (* matcht kein /). Alles darunter
|
||||||
|
# (z. B. deploy/governor/governor.py) braucht ein eigenes Muster — sonst kommt es
|
||||||
|
# nach einem Windows-Checkout mit CRLF zurück und der Shebang auf der Box bricht.
|
||||||
|
deploy/**/*.py text eol=lf
|
||||||
|
|
||||||
# Windows-Batch-Wrapper bleiben CRLF.
|
# Windows-Batch-Wrapper bleiben CRLF.
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
|
|||||||
@@ -20,3 +20,6 @@ frontend/dist/avatar.vrm
|
|||||||
box_recon*
|
box_recon*
|
||||||
gemma_swap*
|
gemma_swap*
|
||||||
|
|
||||||
|
# TypeScript-Inkrementalcache (reines Build-Artefakt, maschinenabhängig)
|
||||||
|
frontend/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from routers import (
|
|||||||
eigenleben,
|
eigenleben,
|
||||||
events,
|
events,
|
||||||
gateway_proxy,
|
gateway_proxy,
|
||||||
|
governor,
|
||||||
health,
|
health,
|
||||||
hermes_ui,
|
hermes_ui,
|
||||||
ideen,
|
ideen,
|
||||||
@@ -141,6 +142,7 @@ app.include_router(maintenance.router)
|
|||||||
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
app.include_router(auftragsbuch.router) # Vorschlags-Inbox (Mensch-Gate als Klick)
|
||||||
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
app.include_router(ideen.router) # Ideen-Queue (natives Hermes-Kanban) — Tür der Zentrale
|
||||||
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
app.include_router(chronik.router) # Timeline der autonomen Taten (Announce-Store)
|
||||||
|
app.include_router(governor.router) # Token-Wächter (:8100) — Zählerstand fürs Cockpit
|
||||||
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
app.include_router(eigenleben.router) # „Von allein": Skills + Vorschlags-Bilanz der Box
|
||||||
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
|
||||||
|
|||||||
@@ -55,6 +55,17 @@
|
|||||||
"role": "scout", "name": "GLM-4.6V-Flash", "repo": "ggml-org/GLM-4.6V-Flash-GGUF",
|
"role": "scout", "name": "GLM-4.6V-Flash", "repo": "ggml-org/GLM-4.6V-Flash-GGUF",
|
||||||
"family": "glm", "generation": 4.6, "total_params_b": 9, "active_params_b": 3,
|
"family": "glm", "generation": 4.6, "total_params_b": 9, "active_params_b": 3,
|
||||||
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": true
|
"moe": true, "quant": "Q4_K_M", "ctx": 32768, "tools": true, "vision": true
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"role": "kritiker", "name": "Devstral-Small-2-24B", "repo": "mistralai/Devstral-Small-2-24B-Instruct-2512",
|
||||||
|
"family": "mistral-devstral", "generation": 2.0, "total_params_b": 24, "active_params_b": 24,
|
||||||
|
"moe": false, "quant": "Q4_K_M", "ctx": 65536, "tools": true, "vision": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "kritiker", "name": "GLM-4.7-Flash", "repo": "zai-org/GLM-4.7-Flash",
|
||||||
|
"family": "glm", "generation": 4.7, "total_params_b": 30, "active_params_b": 3,
|
||||||
|
"moe": true, "quant": "Q4_K_XL", "ctx": 65536, "tools": true, "vision": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Governor — Fenster auf den Token-Waechter (:8100).
|
||||||
|
|
||||||
|
Der Governor ist ein eigenstaendiger, absichtlich winziger Proxy ohne Datenbank: er
|
||||||
|
sitzt zwischen den Coding-Agenten und diesem Gateway, zaehlt ehrlich mit (echte
|
||||||
|
`usage.prompt_tokens` aus jeder Antwort) und zieht bei ueberlangen Sitzungen die
|
||||||
|
Notbremse. Hier wird nichts Neues erhoben — nur sein Status-Endpunkt gleichursprünglich
|
||||||
|
fuer die Oberflaeche verfuegbar gemacht, damit das Frontend nicht per CORS auf einen
|
||||||
|
zweiten Port ausweichen muss.
|
||||||
|
|
||||||
|
Faellt der Governor aus, liefert dieser Router `ok: false` statt eines Fehlers: die
|
||||||
|
Kachel zeigt dann „nicht erreichbar" und das Cockpit bleibt heil.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
GOVERNOR_URL = os.environ.get("MC_GOVERNOR_URL", "http://127.0.0.1:8100")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/governor")
|
||||||
|
async def governor_status() -> dict:
|
||||||
|
"""Momentaufnahme des Token-Waechters. Nie werfen — die Kachel darf nie das Cockpit reissen."""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=3.0) as c:
|
||||||
|
r = await c.get(f"{GOVERNOR_URL}/governor/status")
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
data["reachable"] = True
|
||||||
|
return data
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"reachable": False,
|
||||||
|
"error": f"{type(exc).__name__}: {exc}",
|
||||||
|
"url": GOVERNOR_URL,
|
||||||
|
}
|
||||||
+23
-12
@@ -79,26 +79,37 @@ case "$CODE" in
|
|||||||
# Vorlage oder scheitert der Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
|
# Vorlage oder scheitert der Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
|
||||||
# WICHTIG: Contents-API braucht write:repository → PUSH-Token nutzen (das
|
# WICHTIG: Contents-API braucht write:repository → PUSH-Token nutzen (das
|
||||||
# dedizierte Anlage-Token hat u.U. nur write:user).
|
# dedizierte Anlage-Token hat u.U. nur write:user).
|
||||||
AMPEL="$REALHOME/mission-control-v2/deploy/ampel-ci.yml"
|
|
||||||
PUSHTOKEN="$(printf '%s' "$LINE" | sed -nE 's#https://[^:]+:([^@]+)@.*#\1#p')"; PUSHTOKEN="${PUSHTOKEN:-$TOKEN}"
|
PUSHTOKEN="$(printf '%s' "$LINE" | sed -nE 's#https://[^:]+:([^@]+)@.*#\1#p')"; PUSHTOKEN="${PUSHTOKEN:-$TOKEN}"
|
||||||
if [ -r "$AMPEL" ]; then
|
# saat <lokale-vorlage> <pfad-im-repo> <commit-nachricht> <klartext-name>
|
||||||
SEED_CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/v1/repos/$FULL/contents/.gitea/workflows/ci.yml" \
|
saat () {
|
||||||
|
local QUELLE="$1" ZIEL="$2" MSG="$3" NAME="$4" CODE
|
||||||
|
if [ ! -r "$QUELLE" ]; then
|
||||||
|
echo "WARNUNG: Vorlage fehlt ($QUELLE) — Repo ohne $NAME angelegt." >&2
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/api/v1/repos/$FULL/contents/$ZIEL" \
|
||||||
-H "Authorization: token $PUSHTOKEN" -H "Content-Type: application/json" \
|
-H "Authorization: token $PUSHTOKEN" -H "Content-Type: application/json" \
|
||||||
--data "$(python3 - "$AMPEL" <<'PY'
|
--data "$(python3 - "$QUELLE" "$MSG" <<'PY'
|
||||||
import base64, json, sys
|
import base64, json, sys
|
||||||
inhalt = open(sys.argv[1], "rb").read()
|
inhalt = open(sys.argv[1], "rb").read()
|
||||||
print(json.dumps({"content": base64.b64encode(inhalt).decode(),
|
print(json.dumps({"content": base64.b64encode(inhalt).decode(), "message": sys.argv[2]}))
|
||||||
"message": "CI-Ampel (automatisch bei Repo-Anlage eingepflanzt)"}))
|
|
||||||
PY
|
PY
|
||||||
)")"
|
)")"
|
||||||
if [ "$SEED_CODE" = "201" ]; then
|
if [ "$CODE" = "201" ]; then
|
||||||
echo "CI-Ampel eingepflanzt (.gitea/workflows/ci.yml)."
|
echo "$NAME eingepflanzt ($ZIEL)."
|
||||||
else
|
else
|
||||||
echo "WARNUNG: CI-Ampel-Seed antwortete HTTP $SEED_CODE (Repo ist trotzdem da)." >&2
|
echo "WARNUNG: $NAME-Seed antwortete HTTP $CODE (Repo ist trotzdem da)." >&2
|
||||||
fi
|
fi
|
||||||
else
|
}
|
||||||
echo "WARNUNG: Ampel-Vorlage fehlt ($AMPEL) — Repo ohne CI-Ampel angelegt." >&2
|
# JEDES neue Repo wird mit beiden Wächtern geboren:
|
||||||
fi
|
# ci.yml = die AUSSEN-Prüfung (Gitea Actions nach dem Push, Wasserdicht-Runde 22.07.)
|
||||||
|
# VERIFY = die INNEN-Prüfung (das OpenCode-Plugin führt sie nach jeder Etappe aus und
|
||||||
|
# gibt rote Tests dem Agenten sofort zurück, statt sie erst der CI zu zeigen)
|
||||||
|
# Defensiv: scheitert ein Seed, wird die Anlage NICHT abgebrochen, nur gewarnt.
|
||||||
|
saat "$REALHOME/mission-control-v2/deploy/ampel-ci.yml" ".gitea/workflows/ci.yml" \
|
||||||
|
"CI-Ampel (automatisch bei Repo-Anlage eingepflanzt)" "CI-Ampel"
|
||||||
|
saat "$REALHOME/mission-control-v2/deploy/opencode/VERIFY.template" "VERIFY" \
|
||||||
|
"Pruef-Tor (automatisch bei Repo-Anlage eingepflanzt)" "Pruef-Tor"
|
||||||
echo "Repo '$FULL' angelegt (privat, main initialisiert)."
|
echo "Repo '$FULL' angelegt (privat, main initialisiert)."
|
||||||
echo "CLONE ${CLONE}"
|
echo "CLONE ${CLONE}"
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
|
|||||||
+213
-92
@@ -1,34 +1,52 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Governor — duenner, zustandsloser Token-Waechter-Proxy (Phase 0).
|
"""Governor v2 — Token-Waechter-Proxy vor dem MC2-Gateway.
|
||||||
|
|
||||||
Sitzt zwischen einem Coding-Agenten (Aider) und dem Modell-Endpoint (llama-swap
|
Sitzt zwischen den Coding-Agenten (OpenCode/Zed, Nacht-Laeufe, Hermes-Worker) und dem
|
||||||
:8080). Reicht ALLES unveraendert durch — mit einer Ausnahme bei
|
MC2-Gateway (:9001). Reicht ALLES unveraendert durch — mit einer Ausnahme bei
|
||||||
/v1/chat/completions: er schaetzt die Token-Groesse der Anfrage (= Sessiongroesse,
|
/v1/chat/completions: er bestimmt die Groesse der Anfrage (= Sitzungsgroesse, weil die
|
||||||
weil die ganze Historie jede Runde mitkommt) und handelt nach zwei Schwellen:
|
ganze Historie jede Runde mitkommt) und handelt nach zwei Schwellen:
|
||||||
|
|
||||||
est >= SCHWELLE (soft): haengt eine Stopp-Anweisung als letzte User-Nachricht an
|
est >= SOFT: haengt eine Stopp-Anweisung als letzte User-Nachricht an ("SAVEPOINT.md
|
||||||
("SAVEPOINT.md finalisieren + stoppen") und leitet weiter. Das Modell schreibt
|
finalisieren + stoppen") und leitet weiter. Loggt FIRED.
|
||||||
EINEN ehrlichen Abschluss-Savepoint. Loggt FIRED.
|
est >= HART: antwortet SELBST mit einer kurzen Stopp-Nachricht, OHNE das Modell zu
|
||||||
est >= HART-DECKEL (optional): antwortet SELBST mit einer kurzen Stopp-Nachricht,
|
fragen. Verhindert Fassaden jenseits der Grenze. Loggt HARDSTOP.
|
||||||
OHNE das Modell zu fragen. Verhindert, dass ueber die Grenze hinaus
|
|
||||||
weitergearbeitet wird — genau das erzeugte in Tests eine Fassade. 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 nur Standardbibliothek: kein pip, kein venv, laeuft mit System-python3.
|
||||||
Bewusst zustandslos: jede Anfrage wird fuer sich bewertet; keine Sitzungs-DB.
|
Bewusst ohne Datenbank: ein kleiner Ring im Speicher, mehr braucht es nicht.
|
||||||
|
|
||||||
Konfiguration per Umgebungsvariablen (alle optional):
|
Konfiguration per Umgebungsvariablen (alle optional):
|
||||||
GOV_PORT Listen-Port (Default 8100)
|
GOV_PORT Listen-Port (Default 8100)
|
||||||
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 Ziel (Default http://127.0.0.1:9001)
|
||||||
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 25000)
|
GOV_THRESHOLD Soft-Schwelle fuer den Einschub (Default 45000)
|
||||||
GOV_HARD_CEILING Hart-Deckel; 0 = aus (Default: Soft+5000, AN)
|
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 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_LOG Logdatei (zusaetzlich zu stdout) (Default ./governor.log)
|
||||||
GOV_DIRECTIVE Text des Soft-Einschubs (sonst Default unten)
|
GOV_DIRECTIVE Text des Soft-Einschubs
|
||||||
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort (sonst Default unten)
|
GOV_HARDSTOP_MSG Text der Hart-Stopp-Antwort
|
||||||
GOV_ANNOUNCE_URL Lucy-Sprach-Signal-Endpunkt; "" = aus (Default :9001/api/voice/announce)
|
GOV_ANNOUNCE_URL Lucy-Sprach-Signal; "" = aus (Default :9001/api/voice/announce)
|
||||||
GOV_ANNOUNCE_THROTTLE Sekunden zwischen Signalen (Default 300)
|
GOV_ANNOUNCE_THROTTLE Sekunden zwischen Signalen (Default 300)
|
||||||
GOV_ANNOUNCE_TEXT Text des Sprach-Signals (sonst Default)
|
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 http.client
|
||||||
@@ -37,6 +55,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from collections import deque
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -44,16 +63,22 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
PORT = int(os.environ.get("GOV_PORT", "8100"))
|
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")
|
# Ziel ist das MC2-Gateway, NICHT llama-swap direkt: so bleiben MC2s Rollen-Aliase,
|
||||||
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "25000"))
|
# Bild-Weiche und Telemetrie erhalten. Der Governor ist eine Schicht davor, kein Ersatz.
|
||||||
# Hart-Deckel: Default AN (Soft + 5000 = ein Finalisier-Zug Luft), weil der weiche
|
UPSTREAM = os.environ.get("GOV_UPSTREAM", "http://127.0.0.1:9001")
|
||||||
# Schnitt allein bei Weiterarbeit ueber die Grenze eine Fassade erzeugt (Befund P0).
|
THRESHOLD = int(os.environ.get("GOV_THRESHOLD", "45000"))
|
||||||
# Explizit setzbar; GOV_HARD_CEILING=0 schaltet ihn aus.
|
|
||||||
_hard_env = os.environ.get("GOV_HARD_CEILING")
|
_hard_env = os.environ.get("GOV_HARD_CEILING")
|
||||||
HARD_CEILING = (THRESHOLD + 5000) if _hard_env is None else int(_hard_env)
|
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.2"))
|
||||||
|
CALIBRATE = os.environ.get("GOV_CALIBRATE", "1") != "0"
|
||||||
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"))
|
||||||
|
|
||||||
|
# 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 = (
|
DEFAULT_DIRECTIVE = (
|
||||||
"[GOVERNOR — SITZUNGS-LIMIT ERREICHT] Der Kontext dieser Sitzung ist auf ~{est} "
|
"[GOVERNOR — SITZUNGS-LIMIT ERREICHT] Der Kontext dieser Sitzung ist auf ~{est} "
|
||||||
"Tokens gewachsen (Limit {threshold}). Beginne oder setze JETZT KEINE weiteren "
|
"Tokens gewachsen (Limit {threshold}). Beginne oder setze JETZT KEINE weiteren "
|
||||||
@@ -76,12 +101,8 @@ 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_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
|
ANNOUNCE_THROTTLE = float(os.environ.get("GOV_ANNOUNCE_THROTTLE", "300"))
|
||||||
DEFAULT_ANNOUNCE = (
|
DEFAULT_ANNOUNCE = (
|
||||||
"Commander, die Coding-Sitzung wird voll — ungefähr {est} Tokens. Ich sichere den "
|
"Commander, die Coding-Sitzung wird voll — ungefähr {est} Tokens. Ich sichere den "
|
||||||
"Stand im Savepoint; am besten fangen wir gleich frisch an."
|
"Stand im Savepoint; am besten fangen wir gleich frisch an."
|
||||||
@@ -100,9 +121,21 @@ 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()
|
_announce_lock = threading.Lock()
|
||||||
_last_announce = 0.0 # Zeitstempel der letzten Meldung (Drossel)
|
_last_announce = 0.0
|
||||||
_an = urlparse(ANNOUNCE_URL) if ANNOUNCE_URL else None
|
_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:
|
def log(line: str) -> None:
|
||||||
"""Eine Zeile nach stdout UND in die Logdatei (thread-sicher)."""
|
"""Eine Zeile nach stdout UND in die Logdatei (thread-sicher)."""
|
||||||
@@ -117,28 +150,77 @@ def log(line: str) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def estimate_tokens(messages) -> int:
|
def cpt_for(model: str) -> float:
|
||||||
"""Grobe, aber stabile Heuristik: Zeichen aller Nachrichteninhalte / CPT.
|
"""Aktuelles Zeichen-pro-Token-Verhaeltnis fuer ein Modell (gelernt oder Startwert)."""
|
||||||
|
with _state_lock:
|
||||||
|
return _cpt.get(model, CHARS_PER_TOKEN)
|
||||||
|
|
||||||
Gegen die echten prompt_tokens aus der Antwort kalibriert (CPT=3.5 traf am
|
|
||||||
24.07. auf ~1-3 % genau). Zaehlt Text in String- und Multimodal-Listen-Inhalten;
|
def calibrate(model: str, chars: int, exact: int) -> None:
|
||||||
kleiner Aufschlag je Nachricht fuer Rollen-/Template-Overhead.
|
"""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.
|
||||||
"""
|
"""
|
||||||
chars = 0
|
payload = {k: v for k, v in data.items()
|
||||||
for m in messages or []:
|
if k in ("messages", "tools", "tool_choice", "system", "functions")}
|
||||||
chars += 4
|
try:
|
||||||
content = m.get("content") if isinstance(m, dict) else None
|
return len(json.dumps(payload, ensure_ascii=False))
|
||||||
if isinstance(content, str):
|
except (TypeError, ValueError):
|
||||||
chars += len(content)
|
# Fallback: nur Nachrichtentext (nie schlechter als v0.2)
|
||||||
elif isinstance(content, list):
|
chars = 0
|
||||||
for part in content:
|
for m in data.get("messages") or []:
|
||||||
if isinstance(part, dict) and isinstance(part.get("text"), str):
|
c = m.get("content") if isinstance(m, dict) else None
|
||||||
chars += len(part["text"])
|
if isinstance(c, str):
|
||||||
return int(chars / CHARS_PER_TOKEN)
|
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:
|
def _post_announce(est) -> None:
|
||||||
"""POSTet die Meldung an die MC2-Announce-Pipeline. Laeuft im Hintergrund-Thread."""
|
"""POSTet die Meldung an die MC2-Announce-Pipeline (Lucy spricht sie)."""
|
||||||
try:
|
try:
|
||||||
text = (ANNOUNCE_TEXT.replace("{est}", str(est))
|
text = (ANNOUNCE_TEXT.replace("{est}", str(est))
|
||||||
.replace("{threshold}", str(THRESHOLD)))
|
.replace("{threshold}", str(THRESHOLD)))
|
||||||
@@ -158,8 +240,7 @@ def _post_announce(est) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def maybe_announce(est) -> None:
|
def maybe_announce(est) -> None:
|
||||||
"""Sprach-Signal an Lucy ausloesen — gedrosselt, damit die Pro-Runde-Feuerung
|
"""Sprach-Signal an Lucy — gedrosselt (eine Aeusserung je Episode)."""
|
||||||
nicht spammt (eine Aeusserung je Ueberschreitungs-Episode). Best-effort."""
|
|
||||||
if not _an:
|
if not _an:
|
||||||
return
|
return
|
||||||
global _last_announce
|
global _last_announce
|
||||||
@@ -171,14 +252,35 @@ def maybe_announce(est) -> None:
|
|||||||
threading.Thread(target=_post_announce, args=(est,), daemon=True).start()
|
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):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
protocol_version = "HTTP/1.1"
|
protocol_version = "HTTP/1.1"
|
||||||
server_version = "Governor/0.2"
|
server_version = "Governor/2.0"
|
||||||
|
|
||||||
def log_message(self, *args):
|
def log_message(self, *args):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
|
if self.path.split("?", 1)[0].rstrip("/") in ("/governor/status", "/governor"):
|
||||||
|
self._send_json(200, status_payload())
|
||||||
|
return
|
||||||
self._proxy()
|
self._proxy()
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
@@ -194,6 +296,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self._proxy()
|
self._proxy()
|
||||||
|
|
||||||
# -- Kern ---------------------------------------------------------------
|
# -- 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:
|
def _read_body(self) -> bytes:
|
||||||
length = self.headers.get("Content-Length")
|
length = self.headers.get("Content-Length")
|
||||||
if length is None:
|
if length is None:
|
||||||
@@ -206,8 +321,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
def _proxy(self) -> None:
|
def _proxy(self) -> None:
|
||||||
body = self._read_body()
|
body = self._read_body()
|
||||||
path = self.path
|
path = self.path
|
||||||
# Query-String vor der Endpunkt-Erkennung abschneiden (sonst umgeht
|
|
||||||
# z. B. ?api-version=... den Governor).
|
|
||||||
clean_path = path.split("?", 1)[0]
|
clean_path = path.split("?", 1)[0]
|
||||||
is_chat = clean_path.rstrip("/").endswith("/chat/completions")
|
is_chat = clean_path.rstrip("/").endswith("/chat/completions")
|
||||||
|
|
||||||
@@ -215,19 +328,23 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
est = None
|
est = None
|
||||||
streaming = False
|
streaming = False
|
||||||
model = ""
|
model = ""
|
||||||
|
chars = 0
|
||||||
if is_chat and body:
|
if is_chat and body:
|
||||||
action, body, est, streaming, model = self._decide(body)
|
action, body, est, streaming, model, chars = self._decide(body)
|
||||||
if action in ("soft", "hard"):
|
if action in ("soft", "hard"):
|
||||||
maybe_announce(est) # Sprach-Signal an Lucy (gedrosselt)
|
maybe_announce(est)
|
||||||
|
|
||||||
# HARTER STOPP: selbst antworten, Upstream nie fragen.
|
|
||||||
if action == "hard":
|
if action == "hard":
|
||||||
self._send_canned_stop(model, streaming, est)
|
self._send_canned_stop(model, streaming, est)
|
||||||
log(f"chat est={est} thr={THRESHOLD} hard={HARD_CEILING} HARDSTOP "
|
with _state_lock:
|
||||||
f"stream={streaming} status=200")
|
_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
|
return
|
||||||
|
|
||||||
# Header fuer Upstream aufbereiten.
|
|
||||||
out_headers = {}
|
out_headers = {}
|
||||||
for k, v in self.headers.items():
|
for k, v in self.headers.items():
|
||||||
kl = k.lower()
|
kl = k.lower()
|
||||||
@@ -242,7 +359,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
conn = None
|
conn = None
|
||||||
try:
|
try:
|
||||||
conn = http.client.HTTPConnection(UP_HOST, UP_PORT, timeout=600)
|
conn = http.client.HTTPConnection(UP_HOST, UP_PORT, timeout=900)
|
||||||
conn.request(self.command, path, body=body or None, headers=out_headers)
|
conn.request(self.command, path, body=body or None, headers=out_headers)
|
||||||
resp = conn.getresponse()
|
resp = conn.getresponse()
|
||||||
except (OSError, http.client.HTTPException) as exc:
|
except (OSError, http.client.HTTPException) as exc:
|
||||||
@@ -255,7 +372,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(resp.status)
|
self.send_response(resp.status)
|
||||||
for k, v in resp.getheaders():
|
for k, v in resp.getheaders():
|
||||||
kl = k.lower()
|
kl = k.lower()
|
||||||
# hop-by-hop + Laenge raus; Date/Server setzt send_response schon selbst.
|
|
||||||
if kl in HOP_BY_HOP or kl in ("content-length", "date", "server"):
|
if kl in HOP_BY_HOP or kl in ("content-length", "date", "server"):
|
||||||
continue
|
continue
|
||||||
self.send_header(k, v)
|
self.send_header(k, v)
|
||||||
@@ -266,8 +382,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# read1() gibt jedes Upstream-Stueck sofort zurueck (echtes SSE-
|
# read1() gibt jedes Upstream-Stueck sofort zurueck (echtes SSE-
|
||||||
# Durchreichen). read() wuerde bis 64 KB oder Stream-Ende puffern
|
# Durchreichen); read() wuerde puffern und Streaming haengen lassen.
|
||||||
# und streamendes Aider die ganze Generierung haengen lassen.
|
|
||||||
chunk = resp.read1(65536)
|
chunk = resp.read1(65536)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
@@ -283,41 +398,55 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
exact = self._scan_prompt_tokens(tail)
|
exact = self._scan_prompt_tokens(tail)
|
||||||
if is_chat:
|
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 "-"
|
exact_s = str(exact) if exact is not None else "-"
|
||||||
flag = "FIRED" if action == "soft" else "ok"
|
flag = "FIRED" if action == "soft" else ("skip" if action == "defer" else "ok")
|
||||||
log(f"chat est={est} exact={exact_s} thr={THRESHOLD} {flag} "
|
log(f"chat model={model} est={est} exact={exact_s} cpt={cpt_for(model):.2f} "
|
||||||
f"stream={streaming} status={resp.status}")
|
f"thr={THRESHOLD} {flag} stream={streaming} status={resp.status}")
|
||||||
|
|
||||||
def _decide(self, body: bytes):
|
def _decide(self, body: bytes):
|
||||||
"""Aktion bestimmen: passthrough | soft (Einschub) | hard (Selbstantwort).
|
"""Aktion bestimmen. Rueckgabe: (action, body, est, streaming, model, chars)."""
|
||||||
|
|
||||||
Rueckgabe: (action, body, est, streaming, model).
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(body)
|
data = json.loads(body)
|
||||||
except (ValueError, UnicodeDecodeError):
|
except (ValueError, UnicodeDecodeError):
|
||||||
return "passthrough", body, None, False, ""
|
return "passthrough", body, None, False, "", 0
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
return "passthrough", body, None, False, ""
|
return "passthrough", body, None, False, "", 0
|
||||||
|
|
||||||
messages = data.get("messages")
|
messages = data.get("messages")
|
||||||
streaming = bool(data.get("stream"))
|
streaming = bool(data.get("stream"))
|
||||||
model = data.get("model", "") or ""
|
model = (data.get("model") or "").strip()
|
||||||
est = estimate_tokens(messages if isinstance(messages, list) else [])
|
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 HARD_CEILING > 0 and est >= HARD_CEILING:
|
if HARD_CEILING > 0 and est >= HARD_CEILING:
|
||||||
return "hard", body, est, streaming, model
|
return "hard", body, est, streaming, model, chars
|
||||||
|
|
||||||
if est >= THRESHOLD and isinstance(messages, list):
|
if est >= THRESHOLD and isinstance(messages, list):
|
||||||
# Sichere Substitution statt str.format: ein operator-gesetzter
|
if not safe_to_append(messages):
|
||||||
# GOV_DIRECTIVE mit { } (JSON/Code-Beispiel) darf nicht crashen.
|
# Mitten im Werkzeug-Austausch: nicht dazwischenfunken, naechste Runde.
|
||||||
|
return "defer", body, est, streaming, model, chars
|
||||||
directive = (DIRECTIVE.replace("{est}", str(est))
|
directive = (DIRECTIVE.replace("{est}", str(est))
|
||||||
.replace("{threshold}", str(THRESHOLD)))
|
.replace("{threshold}", str(THRESHOLD)))
|
||||||
messages.append({"role": "user", "content": directive})
|
messages.append({"role": "user", "content": directive})
|
||||||
data["messages"] = messages
|
data["messages"] = messages
|
||||||
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model
|
return "soft", json.dumps(data).encode("utf-8"), est, streaming, model, chars
|
||||||
|
|
||||||
return "passthrough", body, est, streaming, model
|
return "passthrough", body, est, streaming, model, chars
|
||||||
|
|
||||||
def _send_canned_stop(self, model: str, streaming: bool, est) -> None:
|
def _send_canned_stop(self, model: str, streaming: bool, est) -> None:
|
||||||
"""OpenAI-kompatible Stopp-Antwort selbst erzeugen (kein Upstream-Call)."""
|
"""OpenAI-kompatible Stopp-Antwort selbst erzeugen (kein Upstream-Call)."""
|
||||||
@@ -366,16 +495,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def _safe_error(self, status: int, msg: str) -> None:
|
def _safe_error(self, status: int, msg: str) -> None:
|
||||||
try:
|
self._send_json(status, {"error": msg})
|
||||||
data = json.dumps({"error": msg}).encode()
|
|
||||||
self.send_response(status)
|
|
||||||
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)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _scan_prompt_tokens(tail: bytearray):
|
def _scan_prompt_tokens(tail: bytearray):
|
||||||
@@ -405,8 +525,9 @@ def main() -> int:
|
|||||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||||
server.daemon_threads = True
|
server.daemon_threads = True
|
||||||
hard = HARD_CEILING if HARD_CEILING > 0 else "aus"
|
hard = HARD_CEILING if HARD_CEILING > 0 else "aus"
|
||||||
log(f"Governor startet auf {HOST}:{PORT} -> {UPSTREAM} | Soft={THRESHOLD} "
|
log(f"Governor v2 startet auf {HOST}:{PORT} -> {UPSTREAM} | Soft={THRESHOLD} "
|
||||||
f"Hart={hard} | CPT={CHARS_PER_TOKEN} | Log={LOG_PATH}")
|
f"Hart={hard} | CPT-Start={CHARS_PER_TOKEN} kalibrierend={CALIBRATE} | "
|
||||||
|
f"ausgenommen={sorted(EXEMPT_MODELS)} | Log={LOG_PATH}")
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[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
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# opencode-lauf.sh — EIN begrenzter Agentenlauf auf der Box, mit denselben Regeln wie in Zed.
|
||||||
|
#
|
||||||
|
# Das ist die Nacht-Seite von „ein Regelwerk, zwei Ausloeser": tagsueber tippst du in Zed,
|
||||||
|
# nachts ruft ein Hermes-Cron dieses Skript. Beide Wege benutzen
|
||||||
|
# * dieselbe OpenCode-Version,
|
||||||
|
# * dieselbe Mannschaft (~/.config/opencode/opencode.json: coder/hermes/kritiker),
|
||||||
|
# * dasselbe Plugin (~/.config/opencode/plugin/mc2-governor.ts: Zaun + Pruef-Tor),
|
||||||
|
# * denselben Token-Waechter (:8100).
|
||||||
|
#
|
||||||
|
# Unterschied zu deploy/worker.sh: worker.sh ist EIN zustandsloser Completion-Aufruf
|
||||||
|
# (Hermes schreibt die Dateien selbst). Hier laeuft ein ECHTER Agent mit Datei-Haenden,
|
||||||
|
# Subagenten und Pruef-Tor — fuer ganze Karten statt fuer Schnipsel.
|
||||||
|
#
|
||||||
|
# Nutzung:
|
||||||
|
# opencode-lauf.sh <repo-pfad> "<auftrag>"
|
||||||
|
# opencode-lauf.sh ~/projekte/foo "Baue X. Halte dich an AGENTS.md."
|
||||||
|
#
|
||||||
|
# Env:
|
||||||
|
# LAUF_TIMEOUT Sekunden Hoechstdauer (Default 3600)
|
||||||
|
# LAUF_LAUT 1 = Lucy spricht mit (Default 0 = still, Nachtbetrieb)
|
||||||
|
# LAUF_AGENT OpenCode-Agent (Default build)
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
REPO="${1:-}"
|
||||||
|
AUFTRAG="${2:-}"
|
||||||
|
TIMEOUT="${LAUF_TIMEOUT:-3600}"
|
||||||
|
AGENT="${LAUF_AGENT:-build}"
|
||||||
|
OC="$HOME/.opencode/bin/opencode"
|
||||||
|
ANNOUNCE="${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}"
|
||||||
|
|
||||||
|
melde () { # melde <betreff> <text> [prioritaet]
|
||||||
|
curl -sf -m 5 -X POST "$ANNOUNCE" -H 'Content-Type: application/json' \
|
||||||
|
--data "$(python3 -c 'import json,sys; print(json.dumps({"subject":sys.argv[1],"text":sys.argv[2],"source":"loop","priority":sys.argv[3]}))' \
|
||||||
|
"$1" "$2" "${3:-silent}")" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -z "$REPO" ] || [ -z "$AUFTRAG" ]; then
|
||||||
|
echo "Nutzung: $0 <repo-pfad> \"<auftrag>\"" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [ ! -d "$REPO" ]; then
|
||||||
|
echo "FEHLER: '$REPO' ist kein Verzeichnis." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [ ! -x "$OC" ]; then
|
||||||
|
echo "FEHLER: OpenCode nicht gefunden ($OC)." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Der Governor MUSS stehen — ohne ihn liefe der Lauf ohne Sitzungs-Bremse.
|
||||||
|
if ! curl -sf -m 5 -o /dev/null "http://127.0.0.1:8100/governor/status"; then
|
||||||
|
echo "FEHLER: Governor (:8100) antwortet nicht — Lauf abgebrochen (keine Sitzungs-Bremse)." >&2
|
||||||
|
melde "[Lauf]" "Ich habe einen Nachtlauf abgebrochen: der Token-Waechter antwortet nicht." "normal"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Nachts still: Meldungen landen im Briefkasten, Lucy spricht sie aber nicht aus.
|
||||||
|
# Das Plugin liest diese Variable; die Morgen-Zusammenfassung kommt vom Daily-Briefing.
|
||||||
|
if [ "${LAUF_LAUT:-0}" = "1" ]; then export MC2_LOOP_SILENT=0; else export MC2_LOOP_SILENT=1; fi
|
||||||
|
|
||||||
|
LOGDIR="$HOME/.hermes/logs"; mkdir -p "$LOGDIR"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
LOG="$LOGDIR/opencode-lauf-$STAMP.log"
|
||||||
|
MAXRUNDEN="${LAUF_MAX_RUNDEN:-3}"
|
||||||
|
|
||||||
|
cd "$REPO" || exit 2
|
||||||
|
NAME="$(basename "$REPO")"
|
||||||
|
melde "[Lauf]" "Ich fange an zu bauen: $NAME." "silent"
|
||||||
|
echo "=== Lauf $STAMP · Repo $REPO · Agent $AGENT · Timeout ${TIMEOUT}s ===" | tee "$LOG"
|
||||||
|
|
||||||
|
# Verify-Befehl des Projekts lesen (gleiche Datei und gleiche Regeln wie im Plugin).
|
||||||
|
verify_cmd () {
|
||||||
|
[ -r "$REPO/VERIFY" ] || return 1
|
||||||
|
grep -vE '^\s*(#|$)' "$REPO/VERIFY" | paste -sd' && ' -
|
||||||
|
}
|
||||||
|
|
||||||
|
START=$(date +%s)
|
||||||
|
CODE=0
|
||||||
|
RUNDE=0
|
||||||
|
AUFGABE="$AUFTRAG"
|
||||||
|
|
||||||
|
# ── Bau-Schleife ────────────────────────────────────────────────────────────
|
||||||
|
# Warum hier UND im Plugin? Das Plugin haengt am Ereignis `session.idle` — in Zed
|
||||||
|
# laeuft der Prozess weiter und alles ist gut. Bei `opencode run` beendet sich der
|
||||||
|
# Prozess aber, bevor das Pruef-Tor fertig ist (gemessen 25.07.). Fuer unbeaufsichtigte
|
||||||
|
# Laeufe muss die Schleife deshalb HIER liegen, wo sie den Prozess ueberlebt.
|
||||||
|
while :; do
|
||||||
|
RUNDE=$((RUNDE + 1))
|
||||||
|
echo "--- Runde $RUNDE/$MAXRUNDEN ---" | tee -a "$LOG"
|
||||||
|
timeout "$TIMEOUT" "$OC" run --agent "$AGENT" "$AUFGABE" >>"$LOG" 2>&1
|
||||||
|
CODE=$?
|
||||||
|
[ "$CODE" -eq 124 ] && { echo "ZEITUEBERSCHREITUNG" | tee -a "$LOG"; break; }
|
||||||
|
|
||||||
|
VCMD="$(verify_cmd)" || { echo "Kein VERIFY — Pruef-Tor aus, Lauf endet." | tee -a "$LOG"; break; }
|
||||||
|
|
||||||
|
echo "--- Pruef-Tor: $VCMD ---" | tee -a "$LOG"
|
||||||
|
VOUT="$(cd "$REPO" && eval "$VCMD" 2>&1)"; VCODE=$?
|
||||||
|
printf '%s\n' "$VOUT" | tail -20 >> "$LOG"
|
||||||
|
|
||||||
|
if [ "$VCODE" -eq 0 ]; then
|
||||||
|
echo "PRUEF-TOR GRUEN" | tee -a "$LOG"
|
||||||
|
melde "[Pruefung]" "$NAME: Tests gruen nach $RUNDE Runde(n)." "normal"
|
||||||
|
CODE=0
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$RUNDE" -ge "$MAXRUNDEN" ]; then
|
||||||
|
echo "PRUEF-TOR ROT — Reparaturrunden aufgebraucht." | tee -a "$LOG"
|
||||||
|
melde "[Pruefung]" "$NAME: Tests bleiben rot nach $RUNDE Runden. Hier komme ich allein nicht weiter." "normal"
|
||||||
|
CODE=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "PRUEF-TOR ROT — Runde $((RUNDE + 1)) folgt." | tee -a "$LOG"
|
||||||
|
melde "[Pruefung]" "$NAME: Tests rot, ich repariere selbst weiter (Runde $((RUNDE + 1))/$MAXRUNDEN)." "silent"
|
||||||
|
AUFGABE="[MC2-PRUEFTOR] Der Verify-Befehl des Projekts ist fehlgeschlagen.
|
||||||
|
|
||||||
|
Befehl: $VCMD
|
||||||
|
|
||||||
|
Ausgabe (Ende):
|
||||||
|
$(printf '%s' "$VOUT" | tail -c 3000)
|
||||||
|
|
||||||
|
Behebe die URSACHE — nicht das Symptom. Schalte keinen Test ab und aendere keine Tests.
|
||||||
|
Urspruenglicher Auftrag war: $AUFTRAG"
|
||||||
|
done
|
||||||
|
|
||||||
|
DAUER=$(( $(date +%s) - START ))
|
||||||
|
# Nachweis statt Behauptung: was hat der Lauf im Arbeitsbaum tatsaechlich veraendert?
|
||||||
|
GEAENDERT="$(git -C "$REPO" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
||||||
|
|
||||||
|
case "$CODE" in
|
||||||
|
0) ERG="fertig, Pruefung bestanden" ;;
|
||||||
|
124) ERG="ZEITUEBERSCHREITUNG nach ${TIMEOUT}s" ;;
|
||||||
|
*) ERG="Pruefung NICHT bestanden (Exitcode $CODE)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "=== Ergebnis: $ERG · ${DAUER}s · $RUNDE Runde(n) · $GEAENDERT geaenderte Dateien · Log $LOG ===" | tee -a "$LOG"
|
||||||
|
melde "[Lauf]" "$NAME: $ERG nach $((DAUER/60)) Minuten, $RUNDE Runde(n), $GEAENDERT Dateien angefasst." "silent"
|
||||||
|
exit "$CODE"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# OpenCode-Seite: ein Regelwerk, zwei Auslöser
|
||||||
|
|
||||||
|
Tagsüber tippst du in **Zed** (PC), nachts ruft ein **Hermes-Cron** dasselbe (Box).
|
||||||
|
Beide Wege benutzen dieselbe OpenCode-Version, dieselbe Mannschaft, dasselbe Plugin
|
||||||
|
und denselben Token-Wächter. Der einzige Unterschied ist die Adresse des Governors.
|
||||||
|
|
||||||
|
## Was wohin gehört
|
||||||
|
|
||||||
|
| Datei hier | Ziel auf dem PC | Ziel auf der Box |
|
||||||
|
|---|---|---|
|
||||||
|
| `opencode.pc.json` | `~/.config/opencode/opencode.json` | — |
|
||||||
|
| `opencode.box.json` | — | `~/.config/opencode/opencode.json` |
|
||||||
|
| `plugin/mc2-governor.ts` | `~/.config/opencode/plugin/` | `~/.config/opencode/plugin/` |
|
||||||
|
| `VERIFY.template` | — | wird von `gitea-repo-create.sh` in **jedes neue Repo** als `VERIFY` gesät |
|
||||||
|
|
||||||
|
Der Governor selbst liegt in `../governor/` (Proxy + systemd-Unit), der unbeaufsichtigte
|
||||||
|
Läufer in `../opencode-lauf.sh`.
|
||||||
|
|
||||||
|
## Die Mannschaft
|
||||||
|
|
||||||
|
| Rolle | Modell | Gemessen (25.07.2026) | Warum |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `plan` + `build` | `coder` — Qwen3-Coder-Next | **51,5 t/s** | Hält den Faden, verteilt Zuarbeit. MoE mit 3B aktiv → schnell auf Strix Halo. |
|
||||||
|
| `explore` | `hermes` — Qwen3.6-35B | **69,6 t/s** | Ohnehin dauerwarm → kostet **null** zusätzlichen Speicher. Sucht, liest, meldet kurz zurück. |
|
||||||
|
| `review` | `kritiker` — Devstral-Small-2 | **15,0 t/s** | Bewusst eine **fremde Modellfamilie** (Mistral statt Qwen) → andere blinde Flecken. Dicht = langsam beim Schreiben, aber ein Kritiker liest viel und schreibt wenig. |
|
||||||
|
|
||||||
|
`heavy` (gpt-oss-120b, 63 GB) ist **nicht** mehr in der Tagesrolle: es würde beim Laden
|
||||||
|
das ganze warme Set verdrängen. Es bleibt der Nacht-Gutachter (4:30-Cron).
|
||||||
|
|
||||||
|
## Nach einer Änderung
|
||||||
|
|
||||||
|
Die Dateien hier sind **Vorlagen**, keine Live-Konfiguration. Nach einer Änderung
|
||||||
|
verteilen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Box
|
||||||
|
scp deploy/opencode/opencode.box.json hitonabi@192.168.178.151:~/.config/opencode/opencode.json
|
||||||
|
scp deploy/opencode/plugin/*.ts hitonabi@192.168.178.151:~/.config/opencode/plugin/
|
||||||
|
# PC (aus dem Repo heraus)
|
||||||
|
cp deploy/opencode/opencode.pc.json ~/.config/opencode/opencode.json
|
||||||
|
cp deploy/opencode/plugin/*.ts ~/.config/opencode/plugin/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zed muss danach neu gestartet werden** — OpenCode liest seine Konfiguration nur beim Start.
|
||||||
|
|
||||||
|
## Fallen, die Zeit gekostet haben
|
||||||
|
|
||||||
|
- **Kein `_comment`-Schlüssel in `opencode.json`.** OpenCode validiert streng und
|
||||||
|
verweigert den Start mit „Unrecognized key". Kommentare gehören in dieses README.
|
||||||
|
- **Das Plugin läuft auf Windows UND Linux.** Deshalb `node:fs` statt `cat` und Buns
|
||||||
|
`${{ raw: cmd }}` statt `bash -lc` — beides fehlt auf Windows bzw. verschluckt den Befehl.
|
||||||
|
- **Bei `opencode run` beendet sich der Prozess, bevor `session.idle` fertig ist**
|
||||||
|
(gemessen 25.07.). Für unbeaufsichtigte Läufe liegt die Prüf-Schleife deshalb
|
||||||
|
zusätzlich in `opencode-lauf.sh`, wo sie den Prozess überlebt. In Zed greift das Plugin.
|
||||||
|
- **Plugin-Verzeichnis:** `plugin/` und `plugins/` werden beide erkannt; wir nutzen `plugin/`.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# VERIFY — wie man dieses Projekt prueft.
|
||||||
|
#
|
||||||
|
# Diese Datei ist das Pruef-Tor. Das MC2-Governor-Plugin fuehrt sie aus, sobald der
|
||||||
|
# Agent "fertig" sagt. Gruen -> Etappe gilt als fertig. Rot -> der Fehler geht
|
||||||
|
# automatisch als naechster Auftrag an den Agenten zurueck, bis zu 3 Runden.
|
||||||
|
#
|
||||||
|
# Regeln:
|
||||||
|
# * Eine Zeile = ein Befehl. Alle Zeilen werden mit && verkettet.
|
||||||
|
# * Zeilen mit # sind Kommentare.
|
||||||
|
# * KEINE Datei VERIFY im Projekt = Pruef-Tor aus (nichts passiert).
|
||||||
|
# * Der Befehl muss ohne Rueckfragen durchlaufen und mit 0 enden, wenn alles gut ist.
|
||||||
|
#
|
||||||
|
# Beispiele (unzutreffende Zeilen loeschen):
|
||||||
|
#
|
||||||
|
# Python: ruff check . && pytest -q
|
||||||
|
# Node/TS: npm run lint && npm test
|
||||||
|
# Frontend: npm run build
|
||||||
|
# Nur Syntax: python -m compileall -q .
|
||||||
|
# Nichts da: git diff --stat (laeuft immer gruen — Platzhalter)
|
||||||
|
|
||||||
|
git diff --stat
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"aibox": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "AI-Box ueber Governor (lokal)",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://127.0.0.1:8100/v1",
|
||||||
|
"apiKey": "local"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"coder": {
|
||||||
|
"name": "coder — Bauen + Planen (Qwen3-Coder-Next, 51,5 t/s)",
|
||||||
|
"limit": { "context": 131072, "output": 16384 }
|
||||||
|
},
|
||||||
|
"hermes": {
|
||||||
|
"name": "hermes — Suchen (Qwen3.6-35B, immer warm, 69,6 t/s)",
|
||||||
|
"limit": { "context": 65536, "output": 8192 }
|
||||||
|
},
|
||||||
|
"kritiker": {
|
||||||
|
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
|
||||||
|
"limit": { "context": 65536, "output": 8192 }
|
||||||
|
},
|
||||||
|
"heavy": {
|
||||||
|
"name": "heavy — Nacht-Gutachter (gpt-oss-120b, verdraengt das warme Set!)",
|
||||||
|
"limit": { "context": 32768, "output": 8192 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"small_model": "aibox/hermes",
|
||||||
|
"agent": {
|
||||||
|
"plan": {
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"webfetch": "allow",
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"ssh *": "deny",
|
||||||
|
"scp *": "deny",
|
||||||
|
"sftp *": "deny",
|
||||||
|
"ssh arcane@192.168.178.162 *": "allow",
|
||||||
|
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
||||||
|
"scp *arcane@192.168.178.162*": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, laeuft auf dem immer warmen Hirn",
|
||||||
|
"model": "aibox/hermes",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, aendert nichts. Laeuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||||
|
"model": "aibox/kritiker",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"provider": {
|
||||||
|
"aibox": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "AI-Box ueber Governor (192.168.178.151:8100)",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://192.168.178.151:8100/v1",
|
||||||
|
"apiKey": "local"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"heavy": {
|
||||||
|
"name": "heavy — Planer (gpt-oss-120b, 32k)",
|
||||||
|
"limit": { "context": 32768, "output": 8192 }
|
||||||
|
},
|
||||||
|
"coder": {
|
||||||
|
"name": "coder — Bauen (Qwen3-Coder-Next, 131k)",
|
||||||
|
"limit": { "context": 131072, "output": 16384 }
|
||||||
|
},
|
||||||
|
"hermes": {
|
||||||
|
"name": "hermes — Erkunden (Qwen3.6, immer warm, 69,6 t/s)",
|
||||||
|
"limit": { "context": 65536, "output": 8192 }
|
||||||
|
},
|
||||||
|
"kritiker": {
|
||||||
|
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 15,0 t/s)",
|
||||||
|
"limit": { "context": 65536, "output": 8192 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"small_model": "aibox/hermes",
|
||||||
|
"agent": {
|
||||||
|
"plan": {
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"model": "aibox/coder",
|
||||||
|
"permission": {
|
||||||
|
"edit": "allow",
|
||||||
|
"webfetch": "allow",
|
||||||
|
"bash": {
|
||||||
|
"*": "allow",
|
||||||
|
"ssh *": "deny",
|
||||||
|
"scp *": "deny",
|
||||||
|
"sftp *": "deny",
|
||||||
|
"ssh arcane@192.168.178.162 *": "allow",
|
||||||
|
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
||||||
|
"scp *arcane@192.168.178.162*": "allow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, läuft auf dem immer warmen Hirn",
|
||||||
|
"model": "aibox/hermes",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
},
|
||||||
|
"review": {
|
||||||
|
"mode": "subagent",
|
||||||
|
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, ändert nichts. Läuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||||
|
"model": "aibox/kritiker",
|
||||||
|
"permission": { "edit": "deny", "bash": "deny" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* MC2-Governor — der "Fahrlehrer" im OpenCode-Agenten.
|
||||||
|
*
|
||||||
|
* Der Governor-Proxy (:8100) ist die Tankuhr: er sieht nur Tokens und zieht die
|
||||||
|
* Notbremse. Dieses Plugin sitzt IM Agenten und sieht alles andere — jeden
|
||||||
|
* Werkzeuggriff, jede Datei, jedes Sitzungsende. Es macht vier Dinge:
|
||||||
|
*
|
||||||
|
* 1. WERKZEUG-ZAUN (tool.execute.before)
|
||||||
|
* Blockt Handgriffe, die ein Agent nie unbeaufsichtigt tun darf: push,
|
||||||
|
* Historie umschreiben, rekursiv loeschen, sudo, Fremd-Hosts. Genau dieser
|
||||||
|
* Zustandsautomat-Zaun hob lokale Modelle in Messungen von 2/10 auf 10/10 —
|
||||||
|
* nicht weil sie schlauer werden, sondern weil sie nicht mehr entgleisen.
|
||||||
|
*
|
||||||
|
* 2. PRUEF-TOR + SCHLEIFE (session.idle)
|
||||||
|
* Sagt der Agent "fertig", laeuft der Verify-Befehl des Projekts (Datei
|
||||||
|
* `VERIFY` im Repo-Wurzelverzeichnis). GRUEN -> Meldung. ROT -> der Fehler
|
||||||
|
* geht als naechster Auftrag automatisch zurueck an den Agenten, bis zu
|
||||||
|
* MC2_LOOP_MAX_ROUNDS mal. Das ist die "Ralph-Schleife", nur mit Bremse.
|
||||||
|
*
|
||||||
|
* 3. SAVEPOINT STATT ZUSAMMENFASSEN (session.compacted)
|
||||||
|
* Beim Komprimieren fallen still die Regeln aus dem Kontext (Paper
|
||||||
|
* "Governance Decay"). Wir schieben stattdessen den Auftrag nach, SAVEPOINT.md
|
||||||
|
* zu schreiben — Wissen lebt in Datei + git, nicht im schrumpfenden Chat.
|
||||||
|
*
|
||||||
|
* 4. STIMME (MC2 /api/voice/announce)
|
||||||
|
* Jedes Ereignis geht mit eigenem Absender `loop` in MC2s Melde-Briefkasten.
|
||||||
|
* Lucy pollt ihn ohnehin und spricht ihn — ohne eine Zeile Lucy-Code.
|
||||||
|
*
|
||||||
|
* Schalter (Umgebungsvariablen):
|
||||||
|
* MC2_BOX_URL MC2-Basis (Default http://192.168.178.151:9001)
|
||||||
|
* MC2_LOOP_AUTOFIX Selbstreparatur (1 = an, Default an)
|
||||||
|
* MC2_LOOP_MAX_ROUNDS max. Reparaturrunden (Default 3)
|
||||||
|
* MC2_LOOP_SILENT 1 = Lucy schweigt (Nachtlauf; Meldungen kommen trotzdem an)
|
||||||
|
* MC2_LOOP_ANNOUNCE 0 = gar keine Meldungen
|
||||||
|
* MC2_FENCE_OFF 1 = Werkzeug-Zaun aus (nur fuer Notfaelle)
|
||||||
|
*
|
||||||
|
* Liegt global unter ~/.config/opencode/plugin/ und wirkt damit in JEDEM Projekt —
|
||||||
|
* am Tag in Zed, nachts im Cron. Ein Regelwerk, zwei Ausloeser.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BOX_URL = process.env.MC2_BOX_URL || "http://192.168.178.151:9001"
|
||||||
|
const AUTOFIX = process.env.MC2_LOOP_AUTOFIX !== "0"
|
||||||
|
const MAX_ROUNDS = parseInt(process.env.MC2_LOOP_MAX_ROUNDS || "3", 10)
|
||||||
|
const SILENT = process.env.MC2_LOOP_SILENT === "1"
|
||||||
|
const ANNOUNCE_ON = process.env.MC2_LOOP_ANNOUNCE !== "0"
|
||||||
|
const FENCE_OFF = process.env.MC2_FENCE_OFF === "1"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbotene Shell-Handgriffe. Bewusst als Muster auf der ROHEN Kommandozeile —
|
||||||
|
* ein Agent, der `git push` in ein `bash -c` verpackt, wird trotzdem erwischt.
|
||||||
|
* Kein Anspruch auf Sandbox-Sicherheit: das ist ein Leitplanken-Zaun gegen
|
||||||
|
* Entgleisen, keine Abwehr gegen einen boesartigen Akteur.
|
||||||
|
*/
|
||||||
|
const FENCE: Array<{ rx: RegExp; why: string }> = [
|
||||||
|
{ rx: /\bgit\s+push\b/, why: "git push — Veroeffentlichen ist Sache des Menschen (oder der CI-Ampel)." },
|
||||||
|
{ rx: /\bgit\s+reset\s+--hard\b/, why: "git reset --hard — verwirft Arbeit unwiederbringlich." },
|
||||||
|
{ rx: /\bgit\s+clean\s+-[a-z]*f/, why: "git clean -f — loescht ungetrackte Dateien unwiederbringlich." },
|
||||||
|
{ rx: /\bgit\s+(rebase|filter-branch|reflog\s+expire)\b/, why: "Historie umschreiben ist tabu." },
|
||||||
|
{ rx: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+\/(?:\s|$)/, why: "rm -rf / — nein." },
|
||||||
|
{ rx: /\brm\s+-[a-zA-Z]*[rf]/, why: "rekursives/erzwungenes Loeschen — bitte gezielt loeschen statt pauschal." },
|
||||||
|
{ rx: /\bsudo\b/, why: "sudo — Rechteausweitung gehoert nicht in einen Agentenlauf." },
|
||||||
|
{ rx: /\b(shutdown|reboot|mkfs|dd\s+if=)/, why: "System-/Datentraeger-Eingriff." },
|
||||||
|
{ rx: /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/, why: "Aus dem Netz laden und direkt ausfuehren — klassischer Fussschuss." },
|
||||||
|
{ rx: /\bssh\s+(?!arcane@192\.168\.178\.162|-o\s+StrictHostKeyChecking=no\s+arcane@)/, why: "ssh nur zur freigegebenen Arcane-VM." },
|
||||||
|
{ rx: /\bnpm\s+publish\b|\btwine\s+upload\b/, why: "Veroeffentlichen von Paketen ist Sache des Menschen." },
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Zaehler je Sitzung: wie viele Selbstreparatur-Runden liefen schon? */
|
||||||
|
const rounds = new Map<string, number>()
|
||||||
|
/** Doppel-Feuern verhindern: session.idle kann mehrfach kommen. */
|
||||||
|
const busy = new Set<string>()
|
||||||
|
|
||||||
|
async function announce(subject: string, text: string, priority: "normal" | "silent" = "normal") {
|
||||||
|
if (!ANNOUNCE_ON) return
|
||||||
|
try {
|
||||||
|
await fetch(`${BOX_URL}/api/voice/announce`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
source: "loop",
|
||||||
|
priority: SILENT ? "silent" : priority,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(4000),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
/* best effort — eine stumme Lucy darf den Bau nie aufhalten */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MC2Governor = async ({ client, $, directory, worktree }: any) => {
|
||||||
|
const root: string = worktree || directory || process.cwd()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify-Befehl des Projekts lesen. Fehlt die Datei, ist das Pruef-Tor AUS.
|
||||||
|
* Bewusst ueber fs statt `cat`: das Plugin laeuft am Tag auf Windows (Zed) und
|
||||||
|
* nachts auf der Box — `cat` gibt es auf Windows nicht zuverlaessig.
|
||||||
|
*/
|
||||||
|
async function readVerify(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { readFile } = await import("node:fs/promises")
|
||||||
|
const { join } = await import("node:path")
|
||||||
|
const raw = await readFile(join(root, "VERIFY"), "utf8")
|
||||||
|
const cmd = raw
|
||||||
|
.split("\n")
|
||||||
|
.map((l: string) => l.trim())
|
||||||
|
.filter((l: string) => l && !l.startsWith("#"))
|
||||||
|
.join(" && ")
|
||||||
|
return cmd || null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify ausfuehren. Rueckgabe: {ok, output} — Ausgabe auf das Wesentliche gekuerzt.
|
||||||
|
* `{ raw: cmd }` schiebt den Befehl UNESCAPED in Buns Shell; ein normales
|
||||||
|
* `${cmd}` wuerde die ganze Zeile als EIN Argument uebergeben und nie laufen.
|
||||||
|
* Buns Shell ist plattformunabhaengig — kein `bash -lc`, das auf Windows fehlt.
|
||||||
|
*/
|
||||||
|
async function runVerify(cmd: string): Promise<{ ok: boolean; out: string }> {
|
||||||
|
try {
|
||||||
|
const res = await $`${{ raw: cmd }}`.cwd(root).nothrow().quiet()
|
||||||
|
const out = `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`
|
||||||
|
return { ok: res.exitCode === 0, out: out.slice(-4000) }
|
||||||
|
} catch (e: any) {
|
||||||
|
return { ok: false, out: String(e?.message ?? e).slice(-4000) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dem laufenden Agenten einen neuen Auftrag schicken (Selbstreparatur-Schleife). */
|
||||||
|
async function sendPrompt(sessionID: string, text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await client.session.prompt({
|
||||||
|
path: { id: sessionID },
|
||||||
|
body: { parts: [{ type: "text", text }] },
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// ── 1. Werkzeug-Zaun ───────────────────────────────────────────────────
|
||||||
|
"tool.execute.before": async (input: any, output: any) => {
|
||||||
|
if (FENCE_OFF) return
|
||||||
|
if (input?.tool !== "bash") return
|
||||||
|
const cmd: string = output?.args?.command ?? ""
|
||||||
|
if (!cmd) return
|
||||||
|
for (const rule of FENCE) {
|
||||||
|
if (rule.rx.test(cmd)) {
|
||||||
|
await announce(
|
||||||
|
"[Zaun]",
|
||||||
|
`Ich habe einen Befehl geblockt: ${rule.why}`,
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
// Werfen = OpenCode bricht genau diesen Werkzeugaufruf ab und gibt dem
|
||||||
|
// Modell den Grund zurueck. Der Agent arbeitet weiter, nur anders.
|
||||||
|
throw new Error(
|
||||||
|
`[MC2-ZAUN] Blockiert: ${rule.why}\n` +
|
||||||
|
`Befehl war: ${cmd}\n` +
|
||||||
|
`Waehle einen anderen Weg. Wenn das wirklich noetig ist, sag es dem Menschen — ` +
|
||||||
|
`er macht es selbst.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── 2.-4. Ereignisse ───────────────────────────────────────────────────
|
||||||
|
event: async ({ event }: any) => {
|
||||||
|
const type: string = event?.type ?? ""
|
||||||
|
const props: any = event?.properties ?? event ?? {}
|
||||||
|
const sessionID: string = props.sessionID || props.sessionId || props.id || ""
|
||||||
|
|
||||||
|
// ── Savepoint statt Zusammenfassen ──────────────────────────────────
|
||||||
|
if (type === "session.compacted" || type === "experimental.session.compacting") {
|
||||||
|
await announce(
|
||||||
|
"[Sitzung]",
|
||||||
|
"Die Sitzung wurde komprimiert — ich lasse den Stand in SAVEPOINT.md sichern.",
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
if (sessionID) {
|
||||||
|
await sendPrompt(
|
||||||
|
sessionID,
|
||||||
|
"[MC2-GOVERNOR] Der Kontext wurde gerade komprimiert — dabei gehen still " +
|
||||||
|
"Regeln und Details verloren. Aktualisiere JETZT SAVEPOINT.md: was wirklich " +
|
||||||
|
"erledigt ist (nur was im Code steht), der genaue naechste Schritt, offene " +
|
||||||
|
"Fragen, Stolpersteine. Committe die Datei. Danach arbeite normal weiter.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pruef-Tor + Selbstreparatur ─────────────────────────────────────
|
||||||
|
if (type !== "session.idle" || !sessionID) return
|
||||||
|
if (busy.has(sessionID)) return
|
||||||
|
|
||||||
|
const cmd = await readVerify()
|
||||||
|
if (!cmd) return // Kein VERIFY im Projekt -> Pruef-Tor bewusst aus.
|
||||||
|
|
||||||
|
busy.add(sessionID)
|
||||||
|
try {
|
||||||
|
const { ok, out } = await runVerify(cmd)
|
||||||
|
const round = rounds.get(sessionID) ?? 0
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
rounds.delete(sessionID)
|
||||||
|
await announce("[Pruefung]", "Etappe fertig und die Tests sind gruen.", "normal")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AUTOFIX || round >= MAX_ROUNDS) {
|
||||||
|
rounds.delete(sessionID)
|
||||||
|
await announce(
|
||||||
|
"[Pruefung]",
|
||||||
|
`Die Tests sind rot und ich habe ${round} Reparaturversuche verbraucht. ` +
|
||||||
|
`Hier komme ich allein nicht weiter, Commander.`,
|
||||||
|
"normal",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rounds.set(sessionID, round + 1)
|
||||||
|
await announce(
|
||||||
|
"[Pruefung]",
|
||||||
|
`Tests rot — ich repariere selbst weiter, Runde ${round + 1} von ${MAX_ROUNDS}.`,
|
||||||
|
"silent",
|
||||||
|
)
|
||||||
|
await sendPrompt(
|
||||||
|
sessionID,
|
||||||
|
`[MC2-PRUEFTOR] Deine Etappe gilt noch NICHT als fertig: der Verify-Befehl des ` +
|
||||||
|
`Projekts ist fehlgeschlagen.\n\n` +
|
||||||
|
`Befehl: ${cmd}\n\n` +
|
||||||
|
`Ausgabe (Ende):\n\`\`\`\n${out}\n\`\`\`\n\n` +
|
||||||
|
`Behebe die Ursache — nicht das Symptom, und schalte keinen Test ab. ` +
|
||||||
|
`Wenn du fertig bist, melde dich normal; ich pruefe dann erneut. ` +
|
||||||
|
`(Reparaturrunde ${round + 1} von ${MAX_ROUNDS}.)`,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
busy.delete(sessionID)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MC2Governor
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-DZ34eLRy.js";/**
|
import{c as A,Y as P,l as B,b as M,u as D,r as z,j as e,Z as T,e as a,$ as g,a0 as b,a1 as W,x as f,a2 as j,W as G,X as I,C as N,g as y,q as i}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as P,j as e,e as v,aa as Ne,ab as he,u as me,b as pe,r as o,a3 as J,L as S,C as ge,U as be,ac as we,V as Y,X as Z,T as R,D as $,M as T,g as B,ad as ve,ae as fe,af as ye,q as ke}from"./index-DZ34eLRy.js";import{L as _}from"./lightbulb-CUD1a5ov.js";import{C as V}from"./code-xml-CCSjVAVc.js";import{S as xe}from"./send-DrbIxJOY.js";import{L as De}from"./layers-a89ag0RB.js";import{R as Se}from"./rotate-ccw-DRxIaGHA.js";/**
|
import{c as P,j as e,e as v,aa as Ne,ab as he,u as me,b as pe,r as o,a3 as J,L as S,C as ge,U as be,ac as we,V as Y,X as Z,T as R,D as $,M as T,g as B,ad as ve,ae as fe,af as ye,q as ke}from"./index-Cm0NCQeJ.js";import{L as _}from"./lightbulb-CBiiGPIh.js";import{C as V}from"./code-xml-p3YDy76e.js";import{S as xe}from"./send-Bc-GXqQz.js";import{L as De}from"./layers-CKJvqHFy.js";import{R as Se}from"./rotate-ccw-DQJileoK.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.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as m,ag as S,r as c,j as e,ah as z,L as g,U as M,ai as C,ad as D,aj as A,ak as B,al as Z,e as E,am as L,u as R,b as q,g as k,q as T}from"./index-DZ34eLRy.js";import{R as j}from"./rotate-ccw-DRxIaGHA.js";/**
|
import{c as m,ag as S,r as c,j as e,ah as z,L as g,U as M,ai as C,ad as D,aj as A,ak as B,al as Z,e as E,am as L,u as R,b as q,g as k,q as T}from"./index-Cm0NCQeJ.js";import{R as j}from"./rotate-ccw-DQJileoK.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.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-DZ34eLRy.js";import{F as O}from"./folder-open-Bb3HvkQd.js";import{C as R}from"./circle-x-Cw3-1mKp.js";import{C as W}from"./copy-aWlMWseD.js";/**
|
import{c as v,r as c,I,J as A,j as e,K as h,x as B,B as H,D as E,M as z,e as M,L as T,N as F,C as K}from"./index-Cm0NCQeJ.js";import{F as O}from"./folder-open-CvG8JCKh.js";import{C as R}from"./circle-x-DT2INJve.js";import{C as W}from"./copy-BzlMSkzh.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.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as h,an as b,r as i,j as e,L as g,ao as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-DZ34eLRy.js";import{B as S}from"./book-open-C27V5HRN.js";import{C as z}from"./circle-x-Cw3-1mKp.js";/**
|
import{c as h,an as b,r as i,j as e,L as g,ao as f,a2 as j,U as k,e as o,D as N,M as v,N as w,g as y}from"./index-Cm0NCQeJ.js";import{B as S}from"./book-open-CMR6fip9.js";import{C as z}from"./circle-x-DT2INJve.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.
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-DZ34eLRy.js";import{B as w}from"./book-open-C27V5HRN.js";import{a as N,L as B,C as P,Z as E}from"./zap-BXgwt99q.js";import{L as R}from"./layers-a89ag0RB.js";import{L as I}from"./lightbulb-CUD1a5ov.js";/**
|
import{c as p,j as e,r as M,x as C,a6 as z,a7 as k,a8 as y,a0 as v,W as L,a9 as G,a2 as T,e as f,U as D}from"./index-Cm0NCQeJ.js";import{B as w}from"./book-open-CMR6fip9.js";import{a as N,L as B,C as P,Z as E}from"./zap-BOjMx7d9.js";import{L as R}from"./layers-CKJvqHFy.js";import{L as I}from"./lightbulb-CBiiGPIh.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.
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-DZ34eLRy.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};
|
import{Y as b,u as f,$ as h,r as d,j as e,e as c,Z as g,T as p,a3 as j,a4 as N,g as v,a5 as w,q as x}from"./index-Cm0NCQeJ.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};
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-CfWXNn-Z.js","assets/index-DZ34eLRy.js","assets/index-CYYdYGeg.css"])))=>i.map(i=>d[i]);
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/GraphView-B853GRcI.js","assets/index-Cm0NCQeJ.js","assets/index-Bkns39Uj.css"])))=>i.map(i=>d[i]);
|
||||||
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-DZ34eLRy.js";import{C as ee}from"./copy-aWlMWseD.js";import{S as _e}from"./send-DrbIxJOY.js";import{B as Ge}from"./book-open-C27V5HRN.js";/**
|
var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):d[i]=c;var J=(d,i,c)=>ke(d,typeof i!="symbol"?i+"":i,c);import{c as h,r as s,j as e,u as ve,b as Ne,O as W,Q as Se,C as X,R as Ce,S as Ee,P as Me,U as O,e as x,V as ze,W as De,X as Z,A as Y,_ as Oe,g as j}from"./index-Cm0NCQeJ.js";import{C as ee}from"./copy-BzlMSkzh.js";import{S as _e}from"./send-Bc-GXqQz.js";import{B as Ge}from"./book-open-CMR6fip9.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.
|
||||||
@@ -34,7 +34,7 @@ var je=Object.defineProperty;var ke=(d,i,c)=>i in d?je(d,i,{enumerable:!0,config
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the ISC license.
|
* This source code is licensed under the ISC license.
|
||||||
* See the LICENSE file in the root directory of this source tree.
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-CfWXNn-Z.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
*/const Pe=h("User",[["path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2",key:"975kel"}],["circle",{cx:"12",cy:"7",r:"4",key:"17ys0d"}]]);class te extends s.Component{constructor(){super(...arguments);J(this,"state",{error:null})}static getDerivedStateFromError(c){return{error:c}}render(){return this.state.error?e.jsxs("div",{className:"h-[480px] rounded-2xl border border-red-500/20 bg-red-500/5 p-6 text-xs text-red-400 overflow-auto",children:[e.jsx("div",{className:"font-semibold mb-2",children:"Graph konnte nicht gerendert werden"}),e.jsx("pre",{className:"whitespace-pre-wrap break-words text-[11px] leading-relaxed",children:this.state.error.message})]}):this.props.children}}const se=s.lazy(()=>Oe(()=>import("./GraphView-B853GRcI.js"),__vite__mapDeps([0,1,2])).then(d=>({default:d.GraphView}))),k=["identity","knowledge","rules","events"],v=new Set(["auto","agent","hermes"]),N={identity:{label:"Identität",icon:Pe,bg:"bg-cyan-500/10",text:"text-cyan-400"},knowledge:{label:"Wissen",icon:De,bg:"bg-indigo-500/10",text:"text-indigo-400"},rules:{label:"Regeln",icon:Le,bg:"bg-violet-500/10",text:"text-violet-400"},events:{label:"Ereignisse",icon:ze,bg:"bg-amber-500/10",text:"text-amber-400"}},ne={label:"Gedächtnis",icon:Ge,text:"text-muted-foreground"},Re={identity:"border-l-cyan-500/80",knowledge:"border-l-indigo-500/80",rules:"border-l-violet-500/80",events:"border-l-amber-500/80"},_={identity:"#00f5ff",knowledge:"#3b82f6",rules:"#d946ef",events:"#fbbf24"},ae=`Hi Hermes! Lass uns ein kurzes Onboarding machen, damit du dich künftig an mich erinnerst. Stell mir nacheinander ein paar kurze Fragen zu:
|
||||||
1) wer ich bin und woran ich gerade arbeite,
|
1) wer ich bin und woran ich gerade arbeite,
|
||||||
2) wie ich angesprochen werden möchte,
|
2) wie ich angesprochen werden möchte,
|
||||||
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
3) meine bevorzugten Tools, Sprachen und Arbeitsweise,
|
||||||
+16
-16
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{ap as y,r as x,g as L,j as e,L as v,S,ae as W,U as C,e as w,aq as E}from"./index-DZ34eLRy.js";import{F as M}from"./folder-open-Bb3HvkQd.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 T(){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(`
|
import{ap as y,r as x,g as L,j as e,L as v,S,ae as W,U as C,e as w,aq as E}from"./index-Cm0NCQeJ.js";import{F as M}from"./folder-open-CvG8JCKh.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 T(){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(`
|
`),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(`
|
`)},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{T as WissenView};
|
`)},"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{T as WissenView};
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-DZ34eLRy.js";/**
|
import{c as a}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c}from"./index-DZ34eLRy.js";/**
|
import{c}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as e}from"./index-DZ34eLRy.js";/**
|
import{c as e}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c}from"./index-DZ34eLRy.js";/**
|
import{c}from"./index-Cm0NCQeJ.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.
|
||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-DZ34eLRy.js";/**
|
import{c as a}from"./index-Cm0NCQeJ.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.
|
||||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+466
File diff suppressed because one or more lines are too long
-461
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-DZ34eLRy.js";/**
|
import{c as a}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-DZ34eLRy.js";/**
|
import{c as t}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as t}from"./index-DZ34eLRy.js";/**
|
import{c as t}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-DZ34eLRy.js";/**
|
import{c as a}from"./index-Cm0NCQeJ.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.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import{c as a}from"./index-DZ34eLRy.js";/**
|
import{c as a}from"./index-Cm0NCQeJ.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.
|
||||||
Vendored
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<title>Mission Control 2.0</title>
|
<title>Mission Control 2.0</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DZ34eLRy.js"></script>
|
<script type="module" crossorigin src="/assets/index-Cm0NCQeJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CYYdYGeg.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bkns39Uj.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { Fuel, ShieldAlert, Check } from "lucide-react"
|
||||||
|
import { useGovernor } from "@/lib/queries"
|
||||||
|
import type { GovernorRun } from "@/lib/api"
|
||||||
|
|
||||||
|
// ── Token-Wächter („Tankuhr mit Handbremse") ────────────────────────────────
|
||||||
|
// Zeigt in Klartext, wie voll die laufende Coding-Sitzung ist. Zwei Marken:
|
||||||
|
// Soft = ab hier bittet der Governor um einen Savepoint (Sitzung sauber beenden)
|
||||||
|
// Hart = ab hier antwortet er selbst und lässt das Modell gar nicht mehr ran
|
||||||
|
// Die Zeile „Schätzung vs. Wahrheit" ist der Ehrlichkeits-Nachweis: bis 25.07. lag der
|
||||||
|
// Governor bei werkzeugdichten Agenten um Faktor 3 daneben, weil er Werkzeug-Schemata
|
||||||
|
// nicht mitzählte. Jetzt lernt er aus den echten prompt_tokens jeder Antwort.
|
||||||
|
|
||||||
|
const fmtNum = (n: number | null | undefined) =>
|
||||||
|
n == null ? "–" : n.toLocaleString("de-DE")
|
||||||
|
|
||||||
|
function ago(ts: number): string {
|
||||||
|
const s = Math.max(0, Date.now() / 1000 - ts)
|
||||||
|
if (s < 60) return `vor ${Math.round(s)} s`
|
||||||
|
if (s < 3600) return `vor ${Math.round(s / 60)} min`
|
||||||
|
return `vor ${Math.round(s / 3600)} h`
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_META: Record<string, { label: string; tone: string }> = {
|
||||||
|
passthrough: { label: "frei", tone: "text-emerald-400" },
|
||||||
|
defer: { label: "wartet", tone: "text-amber-400" },
|
||||||
|
soft: { label: "Savepoint", tone: "text-amber-400" },
|
||||||
|
hard: { label: "STOPP", tone: "text-rose-400" },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Abweichung Schätzung↔Wahrheit in Prozent — der Ehrlichkeits-Messwert. */
|
||||||
|
function drift(run: GovernorRun | undefined): number | null {
|
||||||
|
if (!run?.est || !run?.exact) return null
|
||||||
|
return ((run.est - run.exact) / run.exact) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GovernorCard() {
|
||||||
|
const { data } = useGovernor()
|
||||||
|
|
||||||
|
if (!data?.reachable) {
|
||||||
|
return (
|
||||||
|
<div className="mc-card p-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Fuel className="h-4.5 w-4.5 text-muted-foreground" />
|
||||||
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
Nicht erreichbar. Die Coding-Sitzungen laufen dann ohne Sitzungs-Bremse.
|
||||||
|
<div className="mt-1 font-mono text-[10px] text-muted-foreground/60">
|
||||||
|
{data?.error ?? "keine Antwort von :8100"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const soft = data.soft ?? 0
|
||||||
|
const hard = data.hard ?? null
|
||||||
|
const runs = data.recent ?? []
|
||||||
|
// Nur echte Bau-Läufe füllen die Tankuhr; Lucys Gespräche sind ausgenommen.
|
||||||
|
const lastBuild = runs.find((r) => !(data.exempt_models ?? []).includes(r.model))
|
||||||
|
const füllung = lastBuild?.exact ?? lastBuild?.est ?? 0
|
||||||
|
const skala = Math.max(hard ?? soft * 1.2, soft * 1.2, füllung)
|
||||||
|
const pct = Math.min(100, (füllung / skala) * 100)
|
||||||
|
const abw = drift(runs.find((r) => r.est != null && r.exact != null))
|
||||||
|
const c = data.counters
|
||||||
|
|
||||||
|
const tone =
|
||||||
|
hard && füllung >= hard ? "bg-rose-500"
|
||||||
|
: füllung >= soft ? "bg-amber-500"
|
||||||
|
: "bg-emerald-500"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mc-card p-5">
|
||||||
|
<div className="mb-3 flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Fuel className="h-4.5 w-4.5 text-primary" />
|
||||||
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Wächter</h2>
|
||||||
|
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex items-baseline gap-2">
|
||||||
|
<span className="font-space text-3xl font-bold tracking-tight tabular-nums text-foreground">
|
||||||
|
{fmtNum(füllung)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Tokens in der letzten Bau-Anfrage
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 pt-1 text-right">
|
||||||
|
<div className="text-[11px] font-semibold uppercase tracking-wider text-amber-400">
|
||||||
|
Savepoint ab {fmtNum(soft)}
|
||||||
|
</div>
|
||||||
|
{hard != null && (
|
||||||
|
<div className="text-[11px] font-semibold uppercase tracking-wider text-rose-400">
|
||||||
|
Stopp ab {fmtNum(hard)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tankuhr */}
|
||||||
|
<div className="relative h-3 overflow-hidden rounded-sm bg-muted/25">
|
||||||
|
<div className={`h-full ${tone} transition-all`} style={{ width: `${pct}%` }} />
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 w-px bg-amber-400/80"
|
||||||
|
style={{ left: `${Math.min(100, (soft / skala) * 100)}%` }}
|
||||||
|
title={`Savepoint-Marke ${fmtNum(soft)}`}
|
||||||
|
/>
|
||||||
|
{hard != null && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-y-0 w-px bg-rose-400/80"
|
||||||
|
style={{ left: `${Math.min(100, (hard / skala) * 100)}%` }}
|
||||||
|
title={`Harter Stopp ${fmtNum(hard)}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Zählerstand */}
|
||||||
|
<div className="mt-3 grid grid-cols-4 gap-2 text-center">
|
||||||
|
{[
|
||||||
|
{ k: "Anfragen", v: fmtNum(c?.chat) },
|
||||||
|
{ k: "Savepoints", v: fmtNum(c?.soft) },
|
||||||
|
{ k: "Stopps", v: fmtNum(c?.hard) },
|
||||||
|
{ k: "Tokens ges.", v: fmtNum(c?.tokens_prompt) },
|
||||||
|
].map((x) => (
|
||||||
|
<div key={x.k} className="rounded-sm bg-muted/20 py-1.5">
|
||||||
|
<div className="font-mono text-sm font-semibold tabular-nums text-foreground">{x.v}</div>
|
||||||
|
<div className="text-[10px] uppercase tracking-wider text-muted-foreground/70">{x.k}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ehrlichkeits-Nachweis */}
|
||||||
|
{abw != null && (
|
||||||
|
<div className="mt-3 flex items-center gap-2 rounded-sm bg-muted/15 px-2.5 py-1.5">
|
||||||
|
{Math.abs(abw) < 15
|
||||||
|
? <Check className="h-3.5 w-3.5 shrink-0 text-emerald-400" />
|
||||||
|
: <ShieldAlert className="h-3.5 w-3.5 shrink-0 text-amber-400" />}
|
||||||
|
<span className="text-[11px] text-muted-foreground">
|
||||||
|
Schätzung lag{" "}
|
||||||
|
<span className={Math.abs(abw) < 15 ? "font-semibold text-emerald-400" : "font-semibold text-amber-400"}>
|
||||||
|
{abw > 0 ? "+" : ""}{abw.toFixed(1)} %
|
||||||
|
</span>{" "}
|
||||||
|
neben der Wahrheit — er kalibriert sich aus jeder Antwort selbst nach.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Letzte Läufe */}
|
||||||
|
{runs.length > 0 && (
|
||||||
|
<div className="mt-3 space-y-1">
|
||||||
|
{runs.slice(0, 5).map((r, i) => {
|
||||||
|
const m = ACTION_META[r.action] ?? { label: r.action, tone: "text-muted-foreground" }
|
||||||
|
return (
|
||||||
|
<div key={`${r.t}-${i}`} className="flex items-center gap-2 text-[11px]">
|
||||||
|
<span className="w-14 shrink-0 text-right font-mono text-[10px] text-muted-foreground/60">{ago(r.t)}</span>
|
||||||
|
<span className="w-20 shrink-0 truncate font-medium text-foreground">{r.model || "?"}</span>
|
||||||
|
<span className="flex-1 font-mono tabular-nums text-muted-foreground">
|
||||||
|
{fmtNum(r.exact ?? r.est)}
|
||||||
|
{r.exact != null && r.est != null && (
|
||||||
|
<span className="text-muted-foreground/50"> (geschätzt {fmtNum(r.est)})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className={`shrink-0 font-semibold ${m.tone}`}>{m.label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-3 border-t border-border/30 pt-2 text-[10px] leading-relaxed text-muted-foreground/70">
|
||||||
|
Alle Coding-Agenten laufen durch den Wächter (:8100). Lucys Alltagsmodelle sind
|
||||||
|
ausgenommen — ein Gespräch ist keine Bau-Sitzung und wird nie unterbrochen.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -582,6 +582,40 @@ export interface ChronikResp {
|
|||||||
items: ChronikItem[]
|
items: ChronikItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Governor (Token-Wächter :8100, GET /api/governor) ────────────────────────
|
||||||
|
// Sitzt zwischen Coding-Agenten und Gateway. `est` ist seine Schätzung VOR dem Lauf,
|
||||||
|
// `exact` die echten prompt_tokens AUS der Antwort — die Differenz ist sein Fehler,
|
||||||
|
// und aus ihr lernt er (chars_per_token je Modell). action: passthrough | soft | hard | defer.
|
||||||
|
export interface GovernorRun {
|
||||||
|
t: number
|
||||||
|
model: string
|
||||||
|
est: number | null
|
||||||
|
exact: number | null
|
||||||
|
action: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GovernorStatus {
|
||||||
|
ok: boolean
|
||||||
|
reachable: boolean
|
||||||
|
error?: string
|
||||||
|
upstream?: string
|
||||||
|
soft?: number
|
||||||
|
hard?: number | null
|
||||||
|
uptime_s?: number
|
||||||
|
counters?: {
|
||||||
|
chat: number
|
||||||
|
soft: number
|
||||||
|
hard: number
|
||||||
|
passthrough: number
|
||||||
|
tokens_prompt: number
|
||||||
|
tokens_completion: number
|
||||||
|
}
|
||||||
|
calibration?: Record<string, { chars_per_token: number; samples: number }>
|
||||||
|
calibration_default?: number
|
||||||
|
exempt_models?: string[]
|
||||||
|
recent?: GovernorRun[]
|
||||||
|
}
|
||||||
|
|
||||||
// ── Eigenleben („Von allein": Skills + Vorschlags-Bilanz der Box) ────────────
|
// ── Eigenleben („Von allein": Skills + Vorschlags-Bilanz der Box) ────────────
|
||||||
export interface EigenlebenSkill {
|
export interface EigenlebenSkill {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
type DiscoverResp,
|
type DiscoverResp,
|
||||||
type DraftsResp,
|
type DraftsResp,
|
||||||
type EigenlebenResp,
|
type EigenlebenResp,
|
||||||
|
type GovernorStatus,
|
||||||
type GroupsResp,
|
type GroupsResp,
|
||||||
type HermesBrainResp,
|
type HermesBrainResp,
|
||||||
type Health,
|
type Health,
|
||||||
@@ -60,6 +61,7 @@ export const qk = {
|
|||||||
auftragsbuch: ["auftragsbuch"] as const,
|
auftragsbuch: ["auftragsbuch"] as const,
|
||||||
ideen: ["ideen"] as const,
|
ideen: ["ideen"] as const,
|
||||||
chronik: ["chronik"] as const,
|
chronik: ["chronik"] as const,
|
||||||
|
governor: ["governor"] as const,
|
||||||
eigenleben: ["eigenleben"] as const,
|
eigenleben: ["eigenleben"] as const,
|
||||||
wissen: ["wissen"] as const,
|
wissen: ["wissen"] as const,
|
||||||
zeitmaschine: ["zeitmaschine"] as const,
|
zeitmaschine: ["zeitmaschine"] as const,
|
||||||
@@ -157,6 +159,16 @@ export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell
|
|||||||
select: (d) => d.turns ?? [],
|
select: (d) => d.turns ?? [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Token-Wächter (:8100). Ist er aus, liefert das Backend `reachable:false` statt zu werfen —
|
||||||
|
// deshalb kein Retry-Sturm und kein Fehlerzustand in der Kachel.
|
||||||
|
export const useGovernor = (refetchInterval: number = TAKT.normal) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: qk.governor,
|
||||||
|
queryFn: () => api<GovernorStatus>("/api/governor"),
|
||||||
|
refetchInterval,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
|
||||||
export const useMemoryGraph = (enabled = true) =>
|
export const useMemoryGraph = (enabled = true) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: qk.memoryGraph,
|
queryKey: qk.memoryGraph,
|
||||||
|
|||||||
@@ -84,11 +84,12 @@ export const ROLE_META: RoleMeta[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "coder",
|
role: "coder",
|
||||||
label: "Programmierer (gründlich)",
|
label: "Programmierer",
|
||||||
short: "Code gründlich",
|
short: "Code",
|
||||||
icon: Code,
|
icon: Code,
|
||||||
desc: "Schreibt und prüft Code besonders sorgfältig — für schwere Coding-Aufgaben.",
|
desc: "Baut und plant. Der Kopf der Coding-Mannschaft — er hält den Faden und verteilt Zuarbeit.",
|
||||||
tone: "bg-fuchsia-500/15 text-fuchsia-400 border-fuchsia-500/25",
|
tone: "bg-fuchsia-500/15 text-fuchsia-400 border-fuchsia-500/25",
|
||||||
|
protected: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "scout",
|
role: "scout",
|
||||||
@@ -103,7 +104,7 @@ export const ROLE_META: RoleMeta[] = [
|
|||||||
label: "Kritiker",
|
label: "Kritiker",
|
||||||
short: "Kritiker",
|
short: "Kritiker",
|
||||||
icon: Scale,
|
icon: Scale,
|
||||||
desc: "Die unbestechliche Zweitmeinung — prüft Behauptungen und Patches (anderer Hersteller als das Hirn).",
|
desc: "Liest dem Programmierer nach jeder Etappe gegen — bewusst aus einer FREMDEN Modellfamilie (Mistral statt Qwen), damit er andere blinde Flecken hat.",
|
||||||
tone: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
tone: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { ErinnerungenCard } from "./ErinnerungenCard"
|
|||||||
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
|
||||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||||
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
import { LatencyCard } from "@/components/dashboard/LatencyCard"
|
||||||
|
import { GovernorCard } from "@/components/dashboard/GovernorCard"
|
||||||
|
|
||||||
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
|
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
|
||||||
const openDrawer = (tab: "maintenance" | "logs") =>
|
const openDrawer = (tab: "maintenance" | "logs") =>
|
||||||
@@ -128,6 +129,10 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<LatencyCard />
|
<LatencyCard />
|
||||||
</div>
|
</div>
|
||||||
|
{/* Token-Wächter: wie voll ist die laufende Coding-Sitzung, und wie ehrlich zählt er */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<GovernorCard />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
|
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user