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:
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user