feat: 2 neue seed_rules + Diagnostic-Persistenz fuer agent_stream + chat-backup API
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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/<name>/
|
||||||
|
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/<ts>.json # Run-Logs (append-only)
|
||||||
|
versions/v_<ts>/ # 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/<service>/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_<UPPER_NAME>` 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_<ts>/`. 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/<s>/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
|
## Diagnostic — Selbstcheck-UI und Einstellungen
|
||||||
|
|
||||||
Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
||||||
@@ -352,7 +438,10 @@ Erreichbar unter `http://<VM-IP>:3001`. Teilt das Netzwerk mit der Bridge.
|
|||||||
- **Voice Export/Import**: einzelne Stimmen als `.tar.gz` zwischen Gameboxen mitnehmen
|
- **Voice Export/Import**: einzelne Stimmen als `.tar.gz` zwischen Gameboxen mitnehmen
|
||||||
- **Settings Export/Import**: `voice_config.json` + `highlight_triggers.json` als JSON-Bundle
|
- **Settings Export/Import**: `voice_config.json` + `highlight_triggers.json` als JSON-Bundle
|
||||||
- **Claude Login**: Browser-Terminal zum Einloggen in den Proxy
|
- **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`
|
- **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] **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] **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-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/<service>/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_<NAME>` 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_<ts>/` (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] **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)
|
- [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)
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,54 @@ SEED_RULES: List[dict] = [
|
|||||||
"Standort per /memory/search holen statt ihn als Arg zu erwarten."
|
"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('<service>')`. 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",
|
"migration_key": "seed/skill-rule/external-api-auth-strategy",
|
||||||
"type": "rule",
|
"type": "rule",
|
||||||
|
|||||||
@@ -3035,6 +3035,7 @@
|
|||||||
document.getElementById('live-desktop').style.display = tab === 'desktop' ? 'block' : 'none';
|
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-aria').className = 'tab-btn' + (tab === 'aria' ? ' active' : '');
|
||||||
document.getElementById('live-tab-desktop').className = 'tab-btn' + (tab === 'desktop' ? ' 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) ──────
|
// ── ARIA Live (read-only Mirror der Claude-Code-Session) ──────
|
||||||
@@ -3150,6 +3151,40 @@
|
|||||||
const el = _ariaStreamEl();
|
const el = _ariaStreamEl();
|
||||||
if (el) el.innerHTML = '<div style="color:#555570;font-style:italic;">Geleert.</div>';
|
if (el) el.innerHTML = '<div style="color:#555570;font-style:italic;">Geleert.</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
`<span style="color:#444460;">━━━ ${events.length} fruehere Events (aus ${d.total || '?'} gespeicherten) ━━━</span>`,
|
||||||
|
'#444460',
|
||||||
|
);
|
||||||
|
for (const ev of events) {
|
||||||
|
try { appendAriaStreamEvent(ev); } catch {}
|
||||||
|
}
|
||||||
|
_ariaPushLine(
|
||||||
|
`<span style="color:#444460;">━━━ Ende History — Live ab hier ━━━</span>`,
|
||||||
|
'#444460',
|
||||||
|
);
|
||||||
|
_ariaMaybeScroll();
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
function ariaPanicStop() {
|
function ariaPanicStop() {
|
||||||
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
|
if (!confirm('Wirklich NOT-AUS? Alle aktiven Claude-Subprocesses werden sofort gekillt.')) return;
|
||||||
send({ action: 'aria_panic_stop' });
|
send({ action: 'aria_panic_stop' });
|
||||||
@@ -5454,6 +5489,9 @@
|
|||||||
|
|
||||||
loadThoughtStream();
|
loadThoughtStream();
|
||||||
connectWS();
|
connectWS();
|
||||||
|
// ARIA-Live ist beim Page-Load schon der aktive Sub-Tab.
|
||||||
|
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
|
||||||
|
loadAriaStreamHistory();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -29,6 +29,40 @@ const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true";
|
|||||||
const RVS_TOKEN = process.env.RVS_TOKEN || "";
|
const RVS_TOKEN = process.env.RVS_TOKEN || "";
|
||||||
const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456";
|
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 ───────────────────────────────────────────────
|
// ── State ───────────────────────────────────────────────
|
||||||
const state = {
|
const state = {
|
||||||
gateway: { status: "disconnected", lastError: null, handshakeOk: false },
|
gateway: { status: "disconnected", lastError: null, handshakeOk: false },
|
||||||
@@ -637,6 +671,9 @@ function connectRVS(forcePlain) {
|
|||||||
// Voller Live-Stream der Claude-Code-Session (assistant_text +
|
// Voller Live-Stream der Claude-Code-Session (assistant_text +
|
||||||
// tool_use mit Input + tool_result mit truncated Output). Geht
|
// tool_use mit Input + tool_result mit truncated Output). Geht
|
||||||
// 1:1 an Browser durch — die ARIA-Live-View rendert's.
|
// 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 });
|
broadcast({ type: "agent_stream", payload: msg.payload });
|
||||||
} else if (msg.type === "memory_saved") {
|
} else if (msg.type === "memory_saved") {
|
||||||
// ARIA hat selber etwas in die Qdrant-DB gespeichert (via memory_save Tool).
|
// 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);
|
req.pipe(proxyReq);
|
||||||
return;
|
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") {
|
} else if (req.url === "/api/brain-export" && req.method === "GET") {
|
||||||
// Komplettes Gehirn als tar.gz streamen.
|
// Komplettes Gehirn als tar.gz streamen.
|
||||||
// Schritte: Brain + Qdrant stoppen (saubere Bytes) → tar streamen → wieder starten.
|
// Schritte: Brain + Qdrant stoppen (saubere Bytes) → tar streamen → wieder starten.
|
||||||
|
|||||||
Reference in New Issue
Block a user