7fac17ed9a
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>
280 lines
11 KiB
TypeScript
280 lines
11 KiB
TypeScript
// Zentrale Daten-Hooks (TanStack Query). Kapseln lib/api.ts und liefern Caching,
|
||
// Dedup (gleicher queryKey = eine Anfrage über alle Views), Retry und Polling.
|
||
// Mutations invalidieren gezielt die betroffenen Keys, statt manuell neu zu laden.
|
||
|
||
import { useQuery, useQueryClient, type QueryClient } from "@tanstack/react-query"
|
||
import { sseVerbunden } from "./events"
|
||
import {
|
||
api,
|
||
type AgentStatus,
|
||
type AuftragsbuchResp,
|
||
type ChronikResp,
|
||
type ConnectResp,
|
||
type ConnectHealth,
|
||
type DiscoverResp,
|
||
type DraftsResp,
|
||
type EigenlebenResp,
|
||
type GovernorStatus,
|
||
type GroupsResp,
|
||
type HermesBrainResp,
|
||
type Health,
|
||
type HistoryResp,
|
||
type IdeenLogResp,
|
||
type IdeenResp,
|
||
type Job,
|
||
type Memory,
|
||
type MemoryGraph,
|
||
type ModelsResp,
|
||
type Reminder,
|
||
type RoutingResp,
|
||
type RoutingPolicyMeta,
|
||
type ServicesResp,
|
||
type SystemStatus,
|
||
type TokenStats,
|
||
type UpdatesResp,
|
||
type VoiceTraceResp,
|
||
type WissenResp,
|
||
type ZeitmaschineResp,
|
||
} from "./api"
|
||
|
||
// Zentrale Query-Keys (eine Quelle der Wahrheit für invalidate).
|
||
export const qk = {
|
||
health: ["health"] as const,
|
||
systemStatus: ["system-status"] as const,
|
||
services: ["services"] as const,
|
||
models: ["models"] as const,
|
||
groups: ["groups"] as const,
|
||
routing: ["routing"] as const,
|
||
routingPolicy: ["routing-policy"] as const,
|
||
jobs: ["jobs"] as const,
|
||
tokenStats: ["token-stats"] as const,
|
||
agentStatus: ["agent-status"] as const,
|
||
hermesBrain: ["hermes-brain"] as const,
|
||
updates: ["updates"] as const,
|
||
discover: ["discover"] as const,
|
||
drafts: (target?: string) => ["drafts", target ?? ""] as const,
|
||
connect: (params?: string) => ["connect", params ?? ""] as const,
|
||
connectHealth: ["connect-health"] as const,
|
||
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
|
||
memoryGraph: ["memory-graph"] as const,
|
||
voiceTrace: ["voice-trace"] as const,
|
||
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,
|
||
reminders: ["reminders"] as const,
|
||
}
|
||
|
||
// ── Polling-Takte ────────────────────────────────────────────────────────────
|
||
// EINE Stelle für alle Intervalle. Regel: Die Live-Graphen (System/Token) takten
|
||
// schnell, alles andere gemächlich — Mutationen invalidieren ohnehin gezielt,
|
||
// Polling ist nur das Sicherheitsnetz. Komponenten übergeben KEINE eigenen
|
||
// Intervalle mehr (mehrere Beobachter = kleinstes Intervall gewinnt).
|
||
const TAKT = {
|
||
graph: 3_000, // Live-Graphen im Cockpit (System, Token-Durchsatz)
|
||
schnell: 8_000, // „fühlt sich live an": Modelle, Voice-Trace, Auftragsbuch
|
||
normal: 10_000, // Dienste, Routing, Agent-Status, Health
|
||
gemuetlich: 30_000, // Chronik, Ideen
|
||
traege: 60_000, // Updates-Check, Reminders, Zeitmaschine, HF-Abfragen
|
||
} as const
|
||
|
||
// UMBAU v3 P3a: Läuft der SSE-Eventstrom (/api/events, siehe lib/events.ts), sind die
|
||
// Poller der ereignishaften Quellen nur noch SICHERHEITSNETZ — der Strom invalidiert
|
||
// bei Änderung sofort. Verbunden = Takt ×5; Strom weg = alter Takt (EventSource
|
||
// reconnectet selbst). Live-Graphen/Status bleiben bewusst beim Polling.
|
||
const relax = (ms: number) => () => (sseVerbunden() ? ms * 5 : ms)
|
||
|
||
// Auftragsbuch: Vorschlags-Inbox (Branches + Skill-Kandidaten). Pollt, damit laufende
|
||
// Annahme-Läufe (Status aus der JSON-Datei des Runners) live sichtbar werden.
|
||
export const useAuftragsbuch = (refetchInterval: number = TAKT.schnell) =>
|
||
useQuery({
|
||
queryKey: qk.auftragsbuch,
|
||
queryFn: () => api<AuftragsbuchResp>("/api/auftragsbuch"),
|
||
refetchInterval: relax(refetchInterval),
|
||
})
|
||
|
||
// Ideen-Queue (natives Hermes-Kanban): das Backend cached ~15 s, öfter pollen lohnt nicht.
|
||
export const useIdeen = (refetchInterval: number = TAKT.gemuetlich) =>
|
||
useQuery({
|
||
queryKey: qk.ideen,
|
||
queryFn: () => api<IdeenResp>("/api/ideen"),
|
||
refetchInterval: relax(refetchInterval),
|
||
})
|
||
|
||
// Ideen-Log (Live Worker Log): pollt nur solange Idee triage/running UND offen
|
||
export const useIdeenLog = (id: string, enabled: boolean = false) =>
|
||
useQuery({
|
||
queryKey: ["ideen-log", id],
|
||
queryFn: () => api<IdeenLogResp>(`/api/ideen/${id}/log`),
|
||
enabled,
|
||
refetchInterval: 4000, // alle ~4s
|
||
staleTime: 0,
|
||
})
|
||
|
||
// Limit gehört in den Key: zwei Aufrufer mit verschiedenen Limits würden sich sonst
|
||
// denselben Cache-Eintrag gegenseitig überschreiben. SSE invalidiert das Präfix ["chronik"].
|
||
export const useChronik = (limit = 150, refetchInterval: number = TAKT.gemuetlich) =>
|
||
useQuery({
|
||
queryKey: [...qk.chronik, limit],
|
||
queryFn: () => api<ChronikResp>(`/api/chronik?limit=${limit}`),
|
||
refetchInterval: relax(refetchInterval),
|
||
select: (d) => d.items ?? [],
|
||
})
|
||
|
||
export const useWissen = () =>
|
||
useQuery({ queryKey: qk.wissen, queryFn: () => api<WissenResp>("/api/wissen") })
|
||
|
||
export const useEigenleben = (refetchInterval: number = TAKT.traege) =>
|
||
useQuery({
|
||
queryKey: qk.eigenleben,
|
||
queryFn: () => api<EigenlebenResp>("/api/eigenleben"),
|
||
refetchInterval,
|
||
})
|
||
|
||
export const useZeitmaschine = (refetchInterval: number = TAKT.traege, enabled = true) =>
|
||
useQuery({
|
||
queryKey: qk.zeitmaschine,
|
||
queryFn: () => api<ZeitmaschineResp>("/api/zeitmaschine"),
|
||
refetchInterval,
|
||
enabled,
|
||
})
|
||
|
||
export const useReminders = (refetchInterval: number = TAKT.traege) =>
|
||
useQuery({
|
||
queryKey: qk.reminders,
|
||
queryFn: () => api<{ items: Reminder[] }>("/api/reminders"),
|
||
refetchInterval: relax(refetchInterval),
|
||
select: (d) => d.items ?? [],
|
||
})
|
||
|
||
// Per-Turn-Latenz-Trace (letzte N Voice/Lucy-Turns mit Stufen-Breakdown).
|
||
export const useVoiceTrace = (limit = 12, refetchInterval: number = TAKT.schnell) =>
|
||
useQuery({
|
||
queryKey: qk.voiceTrace,
|
||
queryFn: () => api<VoiceTraceResp>(`/api/voice/trace?limit=${limit}`),
|
||
refetchInterval,
|
||
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,
|
||
queryFn: () => api<MemoryGraph>("/api/memory/graph"),
|
||
enabled,
|
||
})
|
||
|
||
export const useHealth = () =>
|
||
useQuery({ queryKey: qk.health, queryFn: () => api<Health>("/api/health"), refetchInterval: TAKT.normal })
|
||
|
||
export const useSystemStatus = (refetchInterval: number = TAKT.graph) =>
|
||
useQuery({ queryKey: qk.systemStatus, queryFn: () => api<SystemStatus>("/api/system/status"), refetchInterval })
|
||
|
||
// Metrik-Verlauf (Cockpit-Zeitachse): 1h/24h aus dem Backend-Ringpuffer. Nur aktiv,
|
||
// wenn eine Karte gerade diesen Bereich zeigt (enabled) — Live bleibt beim 3s-Store.
|
||
export const useMetricHistory = (minutes: number, enabled = true) =>
|
||
useQuery({
|
||
queryKey: ["metrics-history", minutes],
|
||
queryFn: () => api<HistoryResp>(`/api/system/history?minutes=${minutes}`),
|
||
enabled,
|
||
refetchInterval: 60_000,
|
||
})
|
||
|
||
export const useServices = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.services, queryFn: () => api<ServicesResp>("/api/system/services"), refetchInterval })
|
||
|
||
// Der SSE-Strom invalidiert "models" bei Laden/Entladen → Poller ist nur Sicherheitsnetz.
|
||
export const useModels = (refetchInterval: number = TAKT.schnell) =>
|
||
useQuery({ queryKey: qk.models, queryFn: () => api<ModelsResp>("/api/models"), refetchInterval: relax(refetchInterval) })
|
||
|
||
export const useGroups = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.groups, queryFn: () => api<GroupsResp>("/api/groups"), refetchInterval })
|
||
|
||
export const useRouting = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.routing, queryFn: () => api<RoutingResp>("/api/routing"), refetchInterval })
|
||
|
||
export const useRoutingPolicy = () =>
|
||
useQuery({ queryKey: qk.routingPolicy, queryFn: () => api<RoutingPolicyMeta>("/api/routing/policy") })
|
||
|
||
// Jobs (Downloads) takten etwas schneller, damit Fortschritts-Balken flüssig wirken.
|
||
// `enabled=false` pausiert den Beobachter (z. B. geschlossene System-Schublade) —
|
||
// der Cache bleibt geteilt, es pollt nur niemand umsonst.
|
||
export const useJobs = (refetchInterval = 3_000, enabled = true) =>
|
||
useQuery({
|
||
queryKey: qk.jobs,
|
||
queryFn: () => api<{ jobs: Job[] }>("/api/jobs"),
|
||
refetchInterval,
|
||
enabled,
|
||
select: (d) => d.jobs ?? [],
|
||
})
|
||
|
||
export const useTokenStats = (refetchInterval: number = TAKT.graph) =>
|
||
useQuery({ queryKey: qk.tokenStats, queryFn: () => api<TokenStats>("/api/system/token-stats"), refetchInterval })
|
||
|
||
export const useAgentStatus = (refetchInterval: number = TAKT.normal) =>
|
||
useQuery({ queryKey: qk.agentStatus, queryFn: () => api<AgentStatus>("/api/agent/status"), refetchInterval })
|
||
|
||
// Agent-Hirn (Hermes) + bestes NousResearch-Update. Selten pollen (HF-Abfrage).
|
||
export const useHermesBrain = (refetchInterval: number = TAKT.traege) =>
|
||
useQuery({ queryKey: qk.hermesBrain, queryFn: () => api<HermesBrainResp>("/api/agent/brain"), refetchInterval })
|
||
|
||
// Updates-Check ist teuer (apt/git auf der Box) — träge pollen reicht völlig.
|
||
export const useUpdates = (refetchInterval: number = TAKT.traege) =>
|
||
useQuery({ queryKey: qk.updates, queryFn: () => api<UpdatesResp>("/api/maintenance/updates"), refetchInterval })
|
||
|
||
export const useDiscover = () =>
|
||
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
|
||
|
||
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
|
||
export const useDrafts = (target?: string) =>
|
||
useQuery({
|
||
queryKey: qk.drafts(target),
|
||
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
|
||
enabled: !!target,
|
||
})
|
||
|
||
export const useConnect = (params?: string) =>
|
||
useQuery({
|
||
queryKey: qk.connect(params),
|
||
queryFn: () => api<ConnectResp>(params ? `/api/connect?${params}` : "/api/connect"),
|
||
})
|
||
|
||
// Live-Status der zwei Leitungen (Gateway + Gedächtnis) — alle 15s aktualisiert.
|
||
export const useConnectHealth = () =>
|
||
useQuery({
|
||
queryKey: qk.connectHealth,
|
||
queryFn: () => api<ConnectHealth>("/api/connect/health"),
|
||
refetchInterval: 15000,
|
||
})
|
||
|
||
export const useMemory = (opts?: { q?: string; category?: string; limit?: number }) =>
|
||
useQuery({
|
||
queryKey: qk.memory(opts?.q, opts?.category),
|
||
queryFn: () => {
|
||
const p = new URLSearchParams()
|
||
if (opts?.q) p.set("q", opts.q)
|
||
if (opts?.category) p.set("category", opts.category)
|
||
return api<Memory[]>(`/api/memory?${p}`)
|
||
},
|
||
select: (d) => (opts?.limit ? d.slice(0, opts.limit) : d),
|
||
})
|
||
|
||
/** Nach Mutationen die betroffenen Listen-Queries neu ziehen. */
|
||
export function invalidate(qc: QueryClient, ...keys: (readonly unknown[])[]) {
|
||
for (const key of keys) qc.invalidateQueries({ queryKey: key })
|
||
}
|
||
|
||
export { useQueryClient }
|