From aaaf118cb7f9815529cb8d4127d6f5e25b9ff213 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Fri, 29 May 2026 23:06:56 +0200 Subject: [PATCH] feat: 2 neue seed_rules + Diagnostic-Persistenz fuer agent_stream + chat-backup API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Befund aus chat_backup.jsonl-Analyse heute: ARIA ist 3x auf oauth_authorize gefallen statt oauth_get_token (Stefan musste manuell einloggen), und beim PDF-Skill ist sie nach Stefans "Variante bitte" zu Ad-hoc-Bash-Befehlen auf der VM gedriftet ("ich lass den Code direkt laufen") — Skill wurde unbrauchbar. Beides genau die Antipattern die wir mit den seed_rules abdecken wollten, nur waren die zu schwach formuliert. seed_rules (jetzt 9 statt 7): - oauth-reauth-reflex: bei 401 ZUERST oauth_get_token, NUR bei dessen Fehler oauth_authorize. Stefan zu Re-Login schicken ist das aergerlichste Antipattern (er sitzt im Auto, muss Handy rauskramen). - no-skill-drift: kaputter Skill -> skill_logs + skill_update, NIEMALS zu Ad-hoc-Bash wechseln (Skill wird Karteileiche). Plus: "ich baue dir einen Skill" SAGEN ohne skill_create zu rufen ist verboten — Stefan checkt die Liste und verliert das Vertrauen. agent_stream-Persistenz: - diagnostic/server.js schreibt jeden agent_stream-Event parallel zum Broadcast in /shared/logs/agent_stream.jsonl (soft-cap 50 MB mit half-truncate beim Ueberlauf). - Live-View laedt beim Page-Load + Sub-Tab-Switch die letzten 200 Eintraege via /api/agent-stream. Browser-Reload / Standby verliert damit den Verlauf nicht mehr. Debug-API ohne SSH: - GET /api/chat-backup?lines=N (Default 200, Max 5000) — geparstes JSON der letzten N Zeilen aus chat_backup.jsonl - GET /api/agent-stream?lines=N — gleiches fuer den persistierten Stream README: - Neuer Abschnitt "## Skills — Architektur" mit Skill-Layout, Drei-Stufen-Daten-Modell (OAuth / config_schema / Brain-Daten), Versionierung, Anti-Friedhof, seed_rules (alle 9 aufgelistet). - Diagnostic-Sektion um agent_stream-Persistenz + neue Debug-Endpoints ergaenzt. - Roadmap: Phase B "Skill-Architektur P0-P4" abgehakt. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 97 +++++++++++++++++++++++++++++++++++++++- aria-brain/seed_rules.py | 48 ++++++++++++++++++++ diagnostic/index.html | 38 ++++++++++++++++ diagnostic/server.js | 79 ++++++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9247958..3e8f836 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,92 @@ aria-brain → Antwort → Bridge → RVS → App --- +## Skills — Architektur + +Skills sind ARIAs wiederverwendbare Faehigkeiten. Jeder Skill ist ein +Python-Programm in seinem eigenen `local-venv`. ARIA legt sie selbst via +`skill_create` an, fixt Bugs mit `skill_update`, rollt zur Not zurueck +mit `skill_rollback`. + +### Skill-Layout + +``` +/data/skills// + skill.json # Manifest (Metadata + config_schema + version_history) + run.py # Entry-Point (Python via venv-python) + requirements.txt # pip-Pakete fuer die venv + README.md # Beschreibung + venv/ # automatisch erzeugt + logs/.json # Run-Logs (append-only) + versions/v_/ # archivierte Vorgaengerstaende (vor jedem update_skill) +``` + +### Drei-Stufen-Daten-Modell + +Skills muessen **niemals** Credentials hardcoden. Drei saubere Wege: + +1. **OAuth2-Tokens** (Spotify, Google, GitHub, Reddit, …): Brain haelt + Client-Credentials und macht den Auth-Flow. Skill ruft + `GET {BRAIN_INTERNAL_URL}/oauth//token` und bekommt einen + frischen access_token (Auto-Refresh < 60 s Restzeit). +2. **Statische Werte** (API-Keys, User-IDs, Default-Geraete): Skill + deklariert ein `config_schema` in `skill.json`, Stefan setzt die + Werte in Diagnostic / App, Skill bekommt sie zur Laufzeit als + `CFG_` ENV. +3. **Brain-Daten** (Memories, Skills-Liste, Standort etc.): jeder Skill + kann gegen `BRAIN_INTERNAL_URL` Endpoints wie `/memory/search`, + `/memory/pinned`, `/skills/list` rufen — z.B. ein Wetter-Skill kann + Stefans Standort aus Memories holen statt ihn als Arg zu erwarten. + +### Versionierung mit Rollback + +`update_skill` archiviert den aktuellen Stand vor jeder strukturellen +Aenderung (entry_code, readme, pip_packages, config_schema, args) nach +`versions/v_/`. ARIA-Tools `skill_list_versions` + `skill_rollback` +(+ HTTP `/skills/{name}/versions` + `/rollback`) erlauben Wiederherstellung. +Vor jedem Rollback wird der aktuelle Stand als „safety-snapshot" gesichert +— der Rollback selbst ist also nicht destruktiv. + +UI sowohl in Diagnostic (Skill-Detail → 📦 Versionen) als auch in der App +(SkillBrowser → Detail-Modal). + +### Anti-Skill-Friedhof + +ARIA hat frueher gerne 9 Spotify-Skills mit Suffixen `-v2`, `-aria`, +`-ctl`, `-fixed` gebaut statt einen sauberen zu pflegen. +`skills.create_skill()` rejected jetzt hart: + +- Versions-Suffixe (`-v\d+`, `_v\d+`, `-new`, `-fixed`, `-old`, + `-alt`, `-copy`, `-final`, `-clean`) +- Prefix-Kollisionen (`spotify` existiert → `spotify-aria` rejected) + +Plus die Skill-Regeln (siehe naechster Abschnitt) erinnern ARIA bei jedem +Chat-Turn an die richtigen Patterns. + +### Skill-Regeln (seed_rules) + +`aria-brain/seed_rules.py` enthaelt 9 `type=rule, pinned=true, +source=seed`-Memories, die bei jedem Brain-Start idempotent in die +Vector-DB geschrieben werden (`migration_key`-basiert). Sie tauchen in +jedem Chat-Turn im Hot-Memory-Block auf: + +- **list-before-create** — IMMER `skill_list` vor `skill_create` +- **no-version-suffix** — keine `-v2`/`_v3`-Namen, Versionsverwaltung ist intern +- **update-not-recreate** — defekten Skill mit `skill_update` fixen, nicht neu bauen +- **no-hardcoded-credentials** — OAuth-Tokens via `oauth_get_token`, keine client_secrets im Code +- **config-schema-for-settings** — statische Werte via `config_schema`, nicht hardcoded +- **brain-internal-url** — `BRAIN_INTERNAL_URL` Endpoints inkl. `/oauth//token`, `/memory/search`, `/memory/pinned`, `/skills/list` +- **oauth-reauth-reflex** — bei 401: ZUERST `oauth_get_token` (Auto-Refresh), nur bei dessen Fehler `oauth_authorize` +- **no-skill-drift** — kein Drift vom Skill zu Ad-hoc-Bash-Befehlen. Skill kaputt? `skill_logs` + `skill_update`. Niemals nur SAGEN „ich baue dir einen Skill", wenn `skill_create` nicht wirklich gefeuert wird +- **external-api-auth-strategy** — OAuth2 → `oauth_get_token`, sonst `config_schema`, NIEMALS hardcoden + +Im Gegensatz zu `aria-data/brain-import/` (User-Saatgut, gitignored, +manueller Diagnostic-Klick) gehoeren seed_rules zum Brain-Code und werden +mit jedem Deploy ausgerollt. Editieren = `SEED_RULES`-Liste anpassen, +Brain neu starten. + +--- + ## Diagnostic — Selbstcheck-UI und Einstellungen Erreichbar unter `http://:3001`. Teilt das Netzwerk mit der Bridge. @@ -352,7 +438,10 @@ Erreichbar unter `http://:3001`. Teilt das Netzwerk mit der Bridge. - **Voice Export/Import**: einzelne Stimmen als `.tar.gz` zwischen Gameboxen mitnehmen - **Settings Export/Import**: `voice_config.json` + `highlight_triggers.json` als JSON-Bundle - **Claude Login**: Browser-Terminal zum Einloggen in den Proxy -- **ARIA Live**: read-only Mirror der Claude-Code-Session — alle Tool-Calls + Inputs + Outputs live in einer Monospace-Liste, farbcodiert. Plus ⛔ **Not-Aus**-Button der per RVS einen `cancel_request` mit `hard:true` ausloest → aria-bridge ruft den proxy-internen `/cancel-all` Side-Channel → alle Claude-Subprocesses werden sofort gekillt +- **ARIA Live**: read-only Mirror der Claude-Code-Session — alle Tool-Calls + Inputs + Outputs live in einer Monospace-Liste, farbcodiert. **Persistenz**: jeder `agent_stream`-Event wird parallel in `/shared/logs/agent_stream.jsonl` (soft-cap 50 MB) geschrieben, Live-View laedt beim Tab-Oeffnen / Page-Reload die letzten 200 Eintraege — Browser-Standby wirft nichts mehr weg. Plus ⛔ **Not-Aus**-Button der per RVS einen `cancel_request` mit `hard:true` ausloest → aria-bridge ruft den proxy-internen `/cancel-all` Side-Channel → alle Claude-Subprocesses werden sofort gekillt +- **Debug-API ohne SSH** (Diagnostic-Server, Port 3001): + - `GET /api/chat-backup?lines=N` — letzte N Zeilen aus `chat_backup.jsonl` (Default 200, max 5000) als geparstes JSON. Hilfreich um nachzuvollziehen was ARIA tatsaechlich gemacht hat. + - `GET /api/agent-stream?lines=N` — gleiche Mechanik fuer den persistierten Live-Stream (Tool-Calls + Inputs + Outputs). - **OAuth-Callback-Pipeline**: Caddy davor terminiert TLS via Let's Encrypt, RVS hat einen HTTP-Listener auf demselben Port wie der WebSocket. Provider (Spotify/Dropbox/Discord/...) redirecten den User an `https://{RVS_HOST}/oauth/callback/{service}` → RVS broadcastet als `oauth_callback`-WS-Message → aria-bridge forwarded an Brain → Brain matched `state`, tauscht `code` gegen Token, persistiert in `/shared/config/oauth_tokens.json`. Token-Refresh laeuft automatisch. ARIA hat vier Brain-Tools: **`oauth_register_provider`** (legt URLs eines neuen Providers wie Dropbox/Discord/Notion/... on-demand in `oauth_apps.json` an — Credentials bleiben Stefans Job), `oauth_authorize`, `oauth_get_token`, `oauth_revoke` --- @@ -929,6 +1018,12 @@ docker exec aria-brain curl localhost:8080/memory/stats - [x] **ARIA Live (Diagnostic) + Not-Aus**: read-only Mirror der Claude-Code-Session ersetzt den SSH-Tab. Tool-Calls + Inputs + Outputs (truncated 4 KB) live, farbcodiert. Roter ⛔ Not-Aus-Button schickt `cancel_request` mit `hard:true` → Bridge ruft den proxy-internen `/cancel-all` Side-Channel (Port 3457) → alle Claude-Subprocesses sofort tot. Plus: Idle-Watchdog im Proxy (20 min Inaktivitaet → Subprocess-Kill) + httpx-Timeout-Split im Brain (connect 10s / read 24h) damit lange Pentests durchlaufen - [x] **OAuth2-Pipeline ueber RVS-Callback**: Caddy mit Let's Encrypt vor dem RVS, HTTP-Route `/oauth/callback/{service}` broadcastet als `oauth_callback`-WS-Message, aria-bridge forwarded an Brain, Token landet in `/shared/config/oauth_tokens.json` (mode 0600). ARIAs `oauth_register_provider`-Tool legt neue Provider on-demand an (URLs/scopes, nicht Credentials). Diagnostic + App haben beide Provider-Verwaltung inklusive Custom-Provider-Anlage - [x] **Skill-Mgmt-Tools fuer ARIA**: `skill_update` (Code/README/pip_packages mit venv-Rebuild) + `skill_delete` — verhindert Skill-Friedhof mit `-v2`/`-fixed`-Suffixen. Plus App-seitiger SkillBrowser (Run + Live-Output + Logs der letzten 20 Runs) in Settings → 🛠️ Skills +- [x] **Skill-Architektur P0-P4**: + - `seed_rules` (9 pinned rule-Memories) werden bei jedem Brain-Boot idempotent in die DB geschrieben (`source=seed`, `migration_key`-basiert). Decken Skill-Friedhof, OAuth-Auth-Strategie, no-skill-drift, BRAIN_INTERNAL_URL ab + - Anti-Friedhof-Check in `create_skill`: rejected Versions-Suffixe + Prefix-Kollisionen hart + - Neuer Brain-HTTP-Endpoint `/oauth//token` + `BRAIN_INTERNAL_URL` ENV-Var fuer Skills — Skill ruft Brain fuer frischen Token statt client_secret hardzucoden + - `config_schema` in skill.json + zentrales `/shared/config/skill_configs.json` + `CFG_` ENV beim Run + `skill_set_config` Brain-Tool + UI in Diagnostic & App (TextInput / Switch / password-Felder mit `***SET***`-Masking) + - Versionierung: jeder `skill_update` archiviert vorherigen Stand nach `versions/v_/` (ohne venv/logs). `skill_list_versions` + `skill_rollback` Brain-Tools (mit Safety-Snapshot + auto venv-Rebuild). UI mit Rollback-Button in Diagnostic & App - [x] **Bridge-Hang-Schutz + Voice-Speed persistent**: 3-Schichten-Watchdog (TCP-Keepalive + Asyncio-Watchdog + File-Based Liveness mit Self-Kill), TLS-Fallback klebt nicht mehr beim Reconnect. `xttsSpeed` jetzt im voice_config.json persistiert — greift auch bei Diagnostic-Chats und nach Bridge-Restart - [x] **Bubble-Aktionen in der App**: Long-Press oder ⎘-Icon auf einer Chat-Bubble → Aktions-Menu mit "📋 Ganzen Text teilen" plus pro extrahierte URL/E-Mail/Telefonnummer eine eigene Teilen-Option (System-Share-Sheet → Zwischenablage / Apps / Browser) diff --git a/aria-brain/seed_rules.py b/aria-brain/seed_rules.py index d042943..6ef6254 100644 --- a/aria-brain/seed_rules.py +++ b/aria-brain/seed_rules.py @@ -114,6 +114,54 @@ SEED_RULES: List[dict] = [ "Standort per /memory/search holen statt ihn als Arg zu erwarten." ), }, + { + "migration_key": "seed/skill-rule/oauth-reauth-reflex", + "type": "rule", + "title": "Skill-Regel: OAuth-Re-Auth-Reflex (Refresh statt Re-Login)", + "category": "skills", + "content": ( + "Wenn ein API-Call gegen einen OAuth-Service 401 / 'unauthorized' / " + "'token expired' zurueckgibt: RUFE ZUERST " + "`oauth_get_token('')`. Brain holt entweder den noch " + "gueltigen Token oder refresht ihn automatisch ueber den " + "gespeicherten refresh_token. In 99% der Faelle reicht das.\n" + "\n" + "Nur wenn `oauth_get_token` selbst einen Fehler wirft " + "('refresh failed', 'no refresh_token', 'service nicht " + "konfiguriert'): DANN `oauth_authorize` und Stefan zum Login " + "schicken. Vorher NIEMALS.\n" + "\n" + "Anti-Pattern (Stefan musste so 3x manuell einloggen weil ich " + "das falsch gemacht hatte): bei jedem 401 reflexartig " + "oauth_authorize zu rufen. Das ist das aergerlichste was Du " + "ihm antun kannst — er muss aus dem Auto raus, Handy " + "rauskramen, klicken. Refresh haendelt das Brain transparent, " + "nutze es." + ), + }, + { + "migration_key": "seed/skill-rule/no-skill-drift", + "type": "rule", + "title": "Skill-Regel: kein Drift vom Skill zu Ad-hoc-Bash", + "category": "skills", + "content": ( + "Wenn ein bestehender Skill ein Problem hat (kaputter Output, " + "fehlender Feature-Wunsch, Setup-Error): lies `skill_logs` und " + "`skill_get`, finde das Problem, fixe es mit `skill_update`. " + "\n" + "ABSOLUT VERBOTEN: 'ich lass den Code jetzt einfach direkt auf " + "der VM laufen' / direkt Bash-curl-Befehle ausfuehren statt " + "den Skill anzufassen. Das macht den Skill zur Karteileiche " + "und beim naechsten Mal hast Du wieder nichts. Stefan kann " + "dann auch nichts wiederverwenden (Triggers, App-UI, Logs).\n" + "\n" + "Auch nicht: 'ich baue dir einen Skill' SAGEN ohne tatsaechlich " + "`skill_create` zu rufen. Stefan checkt die Skill-Liste, und " + "wenn er nichts findet, glaubt er dir nie wieder. Wenn Du es " + "sagst, MACH es. Wenn es Probleme gibt (anti-Friedhof-Check, " + "Setup-Error): sag das ehrlich statt zu halluzinieren." + ), + }, { "migration_key": "seed/skill-rule/external-api-auth-strategy", "type": "rule", diff --git a/diagnostic/index.html b/diagnostic/index.html index 79773ad..a67448d 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -3035,6 +3035,7 @@ document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none'; document.getElementById('live-tab-aria').className = 'tab-btn' + (tab === 'aria' ? ' active' : ''); document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' active' : ''); + if (tab === 'aria') loadAriaStreamHistory(); } // ── ARIA Live (read-only Mirror der Claude-Code-Session) ────── @@ -3150,6 +3151,40 @@ const el = _ariaStreamEl(); if (el) el.innerHTML = '
Geleert.
'; } + + // Beim ersten Tab-Oeffnen / Page-Reload: letzte 200 persistierte Events + // aus dem Diagnostic-Server holen. So sind die Live-Bash-Eintraege auch + // dann da wenn der Browser im Standby war. + let _ariaHistoryLoaded = false; + async function loadAriaStreamHistory(lines = 200) { + if (_ariaHistoryLoaded) return; + _ariaHistoryLoaded = true; + try { + const r = await fetch('/api/agent-stream?lines=' + lines); + if (!r.ok) return; + const d = await r.json(); + const events = d.lines || []; + if (!events.length) return; + const el = _ariaStreamEl(); + if (el) { + // Placeholder ('Sobald ARIA aktiv...') wegwerfen wenn vorhanden + const placeholder = el.querySelector('div[style*="italic"]'); + if (placeholder) el.removeChild(placeholder); + } + _ariaPushLine( + `━━━ ${events.length} fruehere Events (aus ${d.total || '?'} gespeicherten) ━━━`, + '#444460', + ); + for (const ev of events) { + try { appendAriaStreamEvent(ev); } catch {} + } + _ariaPushLine( + `━━━ Ende History — Live ab hier ━━━`, + '#444460', + ); + _ariaMaybeScroll(); + } catch (_) {} + } function ariaPanicStop() { if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return; send({ action: 'aria_panic_stop' }); @@ -5454,6 +5489,9 @@ loadThoughtStream(); connectWS(); + // ARIA-Live ist beim Page-Load schon der aktive Sub-Tab. + // History gleich nach Seitenstart laden damit Browser-Reload nichts verliert. + loadAriaStreamHistory(); diff --git a/diagnostic/server.js b/diagnostic/server.js index fd5e5ff..bec7bc3 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -29,6 +29,40 @@ const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true"; const RVS_TOKEN = process.env.RVS_TOKEN || ""; const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456"; +// ── Persistenz fuer agent_stream-Events ────────────────── +// Jeder agent_stream-Event wird parallel zum Broadcast in eine .jsonl +// geschrieben. Live-View laedt beim Tab-Oeffnen die letzten ~200 Zeilen, +// damit Browser-Reload / Standby den Verlauf nicht wegwerfen. Rotation +// haendelt logrotate / manual cleanup — wir cappen hier nur weichweich. +const AGENT_STREAM_LOG = process.env.AGENT_STREAM_LOG || "/shared/logs/agent_stream.jsonl"; +const AGENT_STREAM_MAX_BYTES = 50 * 1024 * 1024; // 50 MB → halten den File handlebar +function appendAgentStream(payload) { + if (!payload || typeof payload !== "object") return; + try { + const line = JSON.stringify({ ts: Date.now(), ...payload }) + "\n"; + // Soft-Cap: bei >50 MB ein Truncate auf den letzten ~25 MB Inhalt + try { + const st = fs.statSync(AGENT_STREAM_LOG); + if (st.size > AGENT_STREAM_MAX_BYTES) { + const half = Math.floor(AGENT_STREAM_MAX_BYTES / 2); + const fd = fs.openSync(AGENT_STREAM_LOG, "r"); + const buf = Buffer.alloc(half); + fs.readSync(fd, buf, 0, half, st.size - half); + fs.closeSync(fd); + // bis zum naechsten Newline springen damit wir keine halbe Zeile haben + const firstNl = buf.indexOf(0x0a); + const start = firstNl >= 0 ? firstNl + 1 : 0; + fs.writeFileSync(AGENT_STREAM_LOG, buf.slice(start)); + } + } catch {} + // Verzeichnis sicherstellen + try { fs.mkdirSync(path.dirname(AGENT_STREAM_LOG), { recursive: true }); } catch {} + fs.appendFileSync(AGENT_STREAM_LOG, line); + } catch (e) { + // Schweigend ignorieren — Persistence darf den Stream nicht blockieren + } +} + // ── State ─────────────────────────────────────────────── const state = { gateway: { status: "disconnected", lastError: null, handshakeOk: false }, @@ -637,6 +671,9 @@ function connectRVS(forcePlain) { // Voller Live-Stream der Claude-Code-Session (assistant_text + // tool_use mit Input + tool_result mit truncated Output). Geht // 1:1 an Browser durch — die ARIA-Live-View rendert's. + // Zusaetzlich persistieren damit Browser-Reload / Standby den + // History-Verlauf nicht wegwirft. + try { appendAgentStream(msg.payload); } catch {} broadcast({ type: "agent_stream", payload: msg.payload }); } else if (msg.type === "memory_saved") { // ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool). @@ -1714,6 +1751,48 @@ const server = http.createServer((req, res) => { }); req.pipe(proxyReq); return; + } else if (req.url.startsWith("/api/chat-backup") && req.method === "GET") { + // Tail des chat_backup.jsonl — fuer Debug-Sessions (was hat ARIA wirklich + // gesagt/getan). ?lines=N (Default 200, Max 5000). + try { + const u = new URL(req.url, "http://localhost"); + const lines = Math.max(1, Math.min(5000, parseInt(u.searchParams.get("lines") || "200", 10) || 200)); + const file = "/shared/config/chat_backup.jsonl"; + let raw = ""; + try { raw = fs.readFileSync(file, "utf-8"); } catch { + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true, file, lines: [] })); + } + const all = raw.split("\n").filter(l => l.trim()); + const tail = all.slice(-lines); + const parsed = tail.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } }); + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true, file, count: parsed.length, total: all.length, lines: parsed })); + } catch (e) { + res.writeHead(500, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: false, error: e.message })); + } + } else if (req.url.startsWith("/api/agent-stream") && req.method === "GET") { + // Tail des persistierten agent_stream.jsonl. Browser-Live-View laedt das + // beim Tab-Oeffnen damit Reload/Standby keine Events mehr wegschmeisst. + try { + const u = new URL(req.url, "http://localhost"); + const lines = Math.max(1, Math.min(5000, parseInt(u.searchParams.get("lines") || "200", 10) || 200)); + const file = AGENT_STREAM_LOG; + let raw = ""; + try { raw = fs.readFileSync(file, "utf-8"); } catch { + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true, file, lines: [] })); + } + const all = raw.split("\n").filter(l => l.trim()); + const tail = all.slice(-lines); + const parsed = tail.map(l => { try { return JSON.parse(l); } catch { return { _raw: l }; } }); + res.writeHead(200, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: true, file, count: parsed.length, total: all.length, lines: parsed })); + } catch (e) { + res.writeHead(500, { "Content-Type": "application/json" }); + return res.end(JSON.stringify({ ok: false, error: e.message })); + } } else if (req.url === "/api/brain-export" && req.method === "GET") { // Komplettes Gehirn als tar.gz streamen. // Schritte: Brain + Qdrant stoppen (saubere Bytes) → tar streamen → wieder starten.