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:
2026-05-29 23:06:56 +02:00
parent 5e1cb2d26a
commit aaaf118cb7
4 changed files with 261 additions and 1 deletions
+38
View File
@@ -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 = '<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() {
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();
</script>
</body>
</html>
+79
View File
@@ -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.