Umbau Stufe 1-5: Governor v2 + OpenCode-Plugin + Subagenten-Mannschaft

Die Intelligenz sitzt nicht mehr NEBEN dem Coden, sondern DRIN. Alles auf der
Box gemessen, nicht angenommen.

Stufe 1 — Speicher-Haushalt (llama-swap-Config, nicht im Repo):
  Coder dauerwarm statt ttl 600 (Kaltstart 23 s -> 491 ms), Devstral als
  'kritiker' verdrahtet, Swap 11 GB -> 0. Zwei Befunde: Kontext 131k->65k spart
  bei Qwen3-Next 0 GB (Hybrid-Attention), und llama-swap haelt nur EINE Gruppe
  resident -> alles Ko-Residente in dieselbe Gruppe. `persistent: true` verhindert
  dabei das Freiraeumen vor grossen Modellen -> ausgeloester Kernel-OOM, behoben
  durch persistent:false + TTLs (Vision laedt jetzt in 10 s statt 117 s + Absturz).

Stufe 2 — Governor v2 (deploy/governor/):
  Steht jetzt IM Pfad (:8100 -> MC2 :9001) statt daneben. Zaehlt den GANZEN
  Anfragekoerper inkl. tools/tool_calls und kalibriert sich aus den echten
  usage.prompt_tokens jeder Antwort nach: Schaetzfehler 200 % -> 0,3 %.
  Soft-Einschub nur noch, wenn die Nachrichtenkette es erlaubt (kein Dazwischen-
  funken in offene tool_calls). Neuer Status-Endpunkt + systemd-Unit.
  Lucys Alltagsmodelle sind vom Schnitt ausgenommen.

Stufe 3 — OpenCode-Plugin (deploy/opencode/plugin/mc2-governor.ts):
  Werkzeug-Zaun (git push, rm -rf, sudo, curl|sh — bewiesen), Pruef-Tor auf
  session.idle mit Selbstreparatur, Savepoint statt Kompression, Meldungen an
  Lucys Briefkasten mit eigenem Absender 'loop'.

Stufe 4 — Mannschaft (opencode.json):
  plan+build -> coder (51,5 t/s) · explore -> hermes (69,6 t/s, warm, gratis) ·
  review -> Devstral (15,0 t/s, FREMDE Modellfamilie gegen blinde Flecken).
  Der 63-GB-Planer faellt aus der Tagesrolle raus.

Stufe 5 — ein Regelwerk, zwei Ausloeser:
  deploy/opencode-lauf.sh faehrt dieselbe Bau-Pruef-Schleife unbeaufsichtigt
  (die Schleife liegt hier UND im Plugin: bei `opencode run` endet der Prozess,
  bevor session.idle fertig ist — gemessen). Bricht ab, wenn der Governor fehlt.
  gitea-repo-create.sh saet jetzt VERIFY neben der CI-Ampel: jedes neue Repo
  wird mit Innen- UND Aussen-Pruefung geboren.

Oberflaeche:
  Token-Waechter-Kachel im Cockpit (Fuellstand, Marken, Ehrlichkeits-Nachweis),
  /api/governor als gleichursprüngliches Fenster, Devstral im Modellkatalog,
  Rollen-Texte auf die neue Mannschaft aktualisiert.

.gitattributes: deploy/**/*.py auf LF (deploy/*.py greift nur eine Ebene tief).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 21:39:32 +02:00
parent 775e862652
commit 7fac17ed9a
43 changed files with 1542 additions and 610 deletions
@@ -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>
)
}
+34
View File
@@ -582,6 +582,40 @@ export interface ChronikResp {
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) ────────────
export interface EigenlebenSkill {
id: string
+12
View File
@@ -14,6 +14,7 @@ import {
type DiscoverResp,
type DraftsResp,
type EigenlebenResp,
type GovernorStatus,
type GroupsResp,
type HermesBrainResp,
type Health,
@@ -60,6 +61,7 @@ export const qk = {
auftragsbuch: ["auftragsbuch"] as const,
ideen: ["ideen"] as const,
chronik: ["chronik"] as const,
governor: ["governor"] as const,
eigenleben: ["eigenleben"] as const,
wissen: ["wissen"] as const,
zeitmaschine: ["zeitmaschine"] as const,
@@ -157,6 +159,16 @@ export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell
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) =>
useQuery({
queryKey: qk.memoryGraph,
+5 -4
View File
@@ -84,11 +84,12 @@ export const ROLE_META: RoleMeta[] = [
},
{
role: "coder",
label: "Programmierer (gründlich)",
short: "Code gründlich",
label: "Programmierer",
short: "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",
protected: true,
},
{
role: "scout",
@@ -103,7 +104,7 @@ export const ROLE_META: RoleMeta[] = [
label: "Kritiker",
short: "Kritiker",
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",
},
]
@@ -18,6 +18,7 @@ import { ErinnerungenCard } from "./ErinnerungenCard"
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
import { LatencyCard } from "@/components/dashboard/LatencyCard"
import { GovernorCard } from "@/components/dashboard/GovernorCard"
// Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event.
const openDrawer = (tab: "maintenance" | "logs") =>
@@ -128,6 +129,10 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<div className="mt-4">
<LatencyCard />
</div>
{/* Token-Wächter: wie voll ist die laufende Coding-Sitzung, und wie ehrlich zählt er */}
<div className="mt-4">
<GovernorCard />
</div>
</section>
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}