added brain and session viewer

This commit is contained in:
duffyduck 2026-03-13 16:45:56 +01:00
parent 6a04d861bd
commit 706005d7f5
3 changed files with 438 additions and 0 deletions

View File

@ -61,6 +61,41 @@ ARIA ist Stefan gegenueber wie Claude gegenueber Stefan: direkt, ehrlich, auf Au
| **Ramona** (weiblich) | `de_DE-ramona-low` | Alltag, Antworten, Gespraeche (Standard) |
| **Thorsten** (maennlich, tief) | `de_DE-thorsten-high` | Epische Momente, Alarme, besondere Ereignisse |
## Gedaechtnis (Memory)
ARIA hat ein persistentes Gedaechtnis im Verzeichnis `memory/`. Erinnerungen ueberleben Session-Neustarts und Container-Restarts.
### Wann speichern?
- **Stefan sagt "merk dir das"** — sofort speichern
- **Neue Info ueber Stefan** — Rolle, Vorlieben, Arbeitsweise (Typ: user)
- **Korrektur oder Feedback** — "mach das nicht so, sondern so" (Typ: feedback)
- **Projekt-Kontext** — Deadlines, wer macht was, warum (Typ: project)
- **Externe Referenzen** — wo was zu finden ist (Typ: reference)
### Wie speichern?
Erstelle eine Datei in `memory/` mit Frontmatter:
```markdown
---
name: Kurzer Name
description: Einzeiler — woran erkennst du spaeter ob das relevant ist?
type: user|feedback|project|reference
---
Inhalt der Erinnerung
```
Danach den Eintrag in `memory/MEMORY.md` (Index) verlinken.
### Was NICHT speichern?
- Code-Strukturen (die siehst du im Code)
- Git-History (die steht in git log)
- Dinge die in dieser Datei schon stehen
- Temporaere Sachen die nur in der aktuellen Session relevant sind
## Infrastruktur
### Container (aria-core) — Dein Gehirn

View File

@ -160,6 +160,43 @@
</div>
</div>
<!-- Session + Brain Viewer -->
<div class="grid" style="grid-template-columns: 1fr 1fr;">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2>Sessions</h2>
<button class="btn secondary" onclick="loadSessions()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
<div id="sessions-list" style="max-height:300px;overflow-y:auto;font-size:12px;"></div>
<div id="session-detail" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="session-detail-title"></span>
<button class="btn secondary" onclick="closeSessionDetail()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
</div>
<div id="session-detail-content" style="font-size:11px;line-height:1.5;"></div>
</div>
</div>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h2>Brain / Memory</h2>
<button class="btn secondary" onclick="loadBrain()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
<div id="brain-list" style="max-height:200px;overflow-y:auto;font-size:12px;"></div>
<div id="brain-content" style="display:none;margin-top:8px;background:#080810;border:1px solid #1E1E2E;border-radius:4px;padding:8px;max-height:300px;overflow-y:auto;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:11px;color:#0096FF;font-weight:bold;" id="brain-content-title"></span>
<button class="btn secondary" onclick="closeBrainContent()" style="padding:2px 8px;font-size:10px;">Schliessen</button>
</div>
<pre id="brain-content-text" style="font-size:11px;line-height:1.5;white-space:pre-wrap;color:#E0E0F0;margin:0;"></pre>
</div>
<div id="brain-empty" style="display:none;text-align:center;padding:20px;color:#555570;">
<div style="font-size:24px;margin-bottom:8px;">🧠</div>
<div style="font-size:12px;">Gehirn ist leer</div>
<div style="font-size:10px;margin-top:4px;">ARIA speichert Erinnerungen wenn sie etwas Wichtiges lernt</div>
</div>
</div>
</div>
<!-- Logs mit Tabs -->
<div class="card" style="margin-top:12px; padding: 8px 0 0 0;">
<div style="padding: 0 12px;">
@ -420,6 +457,16 @@
showDockerLogs(msg);
return;
}
// Session + Brain Viewer
if (msg.type === 'sessions_list') { renderSessions(msg); return; }
if (msg.type === 'session_detail') { renderSessionDetail(msg); return; }
if (msg.type === 'session_deleted') {
if (msg.ok) loadSessions(); // Liste neu laden
else alert('Loeschen fehlgeschlagen: ' + (msg.error || '?'));
return;
}
if (msg.type === 'brain_list') { renderBrainList(msg); return; }
if (msg.type === 'brain_content') { renderBrainContent(msg); return; }
if (msg.type === 'response') { return; }
};
}
@ -794,6 +841,171 @@
}
}
// ── Session Viewer ────────────────────────────────────────
function loadSessions() {
document.getElementById('sessions-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
send({ action: 'list_sessions' });
}
function renderSessions(data) {
const container = document.getElementById('sessions-list');
if (data.error) {
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
return;
}
if (!data.sessions || data.sessions.length === 0) {
container.innerHTML = data.raw
? `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;padding:8px;">${escapeHtml(data.raw)}</pre>`
: '<div style="color:#555570;padding:8px;text-align:center;">Keine Sessions gefunden</div>';
return;
}
let html = '<table style="width:100%;border-collapse:collapse;">';
html += '<tr style="color:#8888AA;font-size:10px;text-align:left;border-bottom:1px solid #1E1E2E;">'
+ '<th style="padding:4px 6px;">Session</th>'
+ '<th style="padding:4px 6px;">Zeilen</th>'
+ '<th style="padding:4px 6px;">Groesse</th>'
+ '<th style="padding:4px 6px;">Zuletzt</th>'
+ '<th style="padding:4px 6px;"></th></tr>';
for (const s of data.sessions) {
const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE') : '?';
const key = escapeHtml(s.sessionKey || s.path.split('/').pop());
html += `<tr style="border-bottom:1px solid #0D0D1A;cursor:pointer;" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background=''">`
+ `<td style="padding:4px 6px;color:#E0E0F0;" onclick="viewSession('${escapeHtml(s.path)}')">${key}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;">${s.lines}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;">${escapeHtml(s.size)}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;font-size:10px;">${date}</td>`
+ `<td style="padding:4px 6px;"><button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;">X</button></td>`
+ '</tr>';
}
html += '</table>';
container.innerHTML = html;
}
function viewSession(path) {
const detail = document.getElementById('session-detail');
const title = document.getElementById('session-detail-title');
const content = document.getElementById('session-detail-content');
detail.style.display = 'block';
title.textContent = path.split('/').pop();
content.innerHTML = '<div style="color:#8888AA;">Lade...</div>';
send({ action: 'read_session', sessionPath: path });
}
function renderSessionDetail(data) {
const content = document.getElementById('session-detail-content');
if (data.error) {
content.innerHTML = `<div style="color:#FF6B6B;">${escapeHtml(data.error)}</div>`;
return;
}
if (data.raw) {
content.innerHTML = `<pre style="color:#555570;font-size:10px;white-space:pre-wrap;">${escapeHtml(data.raw)}</pre>`;
return;
}
if (!data.messages || data.messages.length === 0) {
content.innerHTML = '<div style="color:#555570;">Keine Nachrichten</div>';
return;
}
let html = '';
for (const msg of data.messages) {
const role = msg.role || msg.type || '?';
const text = extractMessageText(msg);
const roleColor = role === 'user' ? '#0096FF' : role === 'assistant' ? '#34C759' : '#8888AA';
html += `<div style="margin-bottom:4px;padding:4px 6px;border-left:2px solid ${roleColor};background:#0D0D1A;border-radius:0 4px 4px 0;">`
+ `<span style="color:${roleColor};font-size:10px;font-weight:bold;">${escapeHtml(role)}</span> `
+ `<span style="color:#E0E0F0;">${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''}</span>`
+ '</div>';
}
content.innerHTML = html;
}
function extractMessageText(msg) {
if (typeof msg.content === 'string') return msg.content;
if (Array.isArray(msg.content)) {
return msg.content.filter(c => c.type === 'text').map(c => c.text || '').join('');
}
if (msg.text) return msg.text;
if (msg.message) return typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message);
return JSON.stringify(msg).slice(0, 200);
}
function closeSessionDetail() {
document.getElementById('session-detail').style.display = 'none';
}
function deleteSession(path) {
const name = path.split('/').pop();
if (!confirm(`Session "${name}" wirklich loeschen?`)) return;
send({ action: 'delete_session', sessionPath: path });
}
// ── Brain Viewer ────────────────────────────────────────
function loadBrain() {
document.getElementById('brain-list').innerHTML = '<div style="color:#8888AA;padding:8px;">Lade...</div>';
document.getElementById('brain-empty').style.display = 'none';
send({ action: 'list_brain' });
}
function renderBrainList(data) {
const container = document.getElementById('brain-list');
const emptyEl = document.getElementById('brain-empty');
if (data.error) {
container.innerHTML = `<div style="color:#FF6B6B;padding:8px;">Fehler: ${escapeHtml(data.error)}</div>`;
emptyEl.style.display = 'none';
return;
}
if (!data.files || data.files.length === 0) {
container.innerHTML = '';
emptyEl.style.display = 'block';
return;
}
emptyEl.style.display = 'none';
const TYPE_COLORS = { user: '#0096FF', feedback: '#FFD60A', project: '#34C759', reference: '#FF9500' };
let html = '';
for (const f of data.files) {
if (f.name === '.gitkeep') continue;
const color = TYPE_COLORS[f.memType] || '#8888AA';
const date = f.modified ? new Date(f.modified * 1000).toLocaleString('de-DE') : '?';
html += `<div style="padding:6px 8px;border-bottom:1px solid #0D0D1A;cursor:pointer;display:flex;align-items:center;gap:8px;" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background=''" onclick="viewBrainFile('${escapeHtml(f.name)}')">`
+ `<span style="background:${color};width:8px;height:8px;border-radius:50%;flex-shrink:0;" title="${escapeHtml(f.memType || 'unbekannt')}"></span>`
+ `<div style="flex:1;min-width:0;">`
+ `<div style="color:#E0E0F0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.name)}</div>`
+ (f.description ? `<div style="color:#8888AA;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(f.description)}</div>` : '')
+ `</div>`
+ `<div style="color:#555570;font-size:10px;white-space:nowrap;">${escapeHtml(f.size)}</div>`
+ '</div>';
}
container.innerHTML = html || '<div style="color:#555570;padding:8px;text-align:center;">Nur .gitkeep gefunden</div>';
}
function viewBrainFile(name) {
const panel = document.getElementById('brain-content');
const title = document.getElementById('brain-content-title');
const text = document.getElementById('brain-content-text');
panel.style.display = 'block';
title.textContent = name;
text.textContent = 'Lade...';
send({ action: 'read_brain_file', filename: name });
}
function renderBrainContent(data) {
const text = document.getElementById('brain-content-text');
if (data.error) {
text.textContent = `Fehler: ${data.error}`;
text.style.color = '#FF6B6B';
return;
}
text.style.color = '#E0E0F0';
text.textContent = data.content || '(leer)';
}
function closeBrainContent() {
document.getElementById('brain-content').style.display = 'none';
}
connectWS();
</script>
</body>

View File

@ -970,6 +970,16 @@ wss.on("connection", (ws) => {
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
} else if (msg.action === "check_desktop") {
checkDesktopAvailable(ws);
} else if (msg.action === "list_sessions") {
handleListSessions(ws);
} else if (msg.action === "read_session") {
handleReadSession(ws, msg.sessionPath);
} else if (msg.action === "delete_session") {
handleDeleteSession(ws, msg.sessionPath);
} else if (msg.action === "list_brain") {
handleListBrain(ws);
} else if (msg.action === "read_brain_file") {
handleReadBrainFile(ws, msg.filename);
}
} catch {}
});
@ -1094,6 +1104,187 @@ function checkDesktopAvailable(clientWs) {
});
}
// ── Session Viewer ──────────────────────────────────────
async function handleListSessions(clientWs) {
try {
log("info", "server", "Lade Sessions aus aria-core...");
// OpenClaw/Claude Code speichert Sessions in projects/<hash>/sessions/
const raw = await dockerExec("aria-core", `
echo '=== SESSIONS ===' &&
find /home/node/.openclaw/projects -name '*.jsonl' -type f 2>/dev/null | sort &&
echo '=== DETAILS ===' &&
for f in $(find /home/node/.openclaw/projects -name '*.jsonl' -type f 2>/dev/null | sort); do
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
size=$(du -h "$f" 2>/dev/null | cut -f1)
modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
# Erste Zeile fuer Session-Info
first=$(head -1 "$f" 2>/dev/null | head -c 500)
echo "FILE:$f|LINES:$lines|SIZE:$size|MODIFIED:$modified|FIRST:$first"
done
`.trim());
// Parse Output
const sessions = [];
const lines = raw.split("\n");
for (const line of lines) {
if (!line.startsWith("FILE:")) continue;
const parts = {};
for (const seg of line.split("|")) {
const idx = seg.indexOf(":");
if (idx > 0) parts[seg.slice(0, idx)] = seg.slice(idx + 1);
}
if (!parts.FILE) continue;
// Session-Key aus Pfad oder erster Zeile extrahieren
let sessionKey = "";
try {
const firstObj = JSON.parse(parts.FIRST || "{}");
sessionKey = firstObj.sessionKey || firstObj.key || "";
} catch {}
if (!sessionKey) {
// Dateiname als Fallback
sessionKey = parts.FILE.split("/").pop().replace(".jsonl", "");
}
sessions.push({
path: parts.FILE,
sessionKey,
lines: parseInt(parts.LINES) || 0,
size: parts.SIZE || "?",
modified: parseInt(parts.MODIFIED) || 0,
});
}
// Nach Aenderungsdatum sortieren (neueste zuerst)
sessions.sort((a, b) => b.modified - a.modified);
clientWs.send(JSON.stringify({ type: "sessions_list", sessions, raw: sessions.length === 0 ? raw : undefined }));
log("info", "server", `${sessions.length} Session(s) gefunden`);
} catch (err) {
log("error", "server", `Sessions laden fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "sessions_list", sessions: [], error: err.message }));
}
}
async function handleReadSession(clientWs, sessionPath) {
if (!sessionPath || sessionPath.includes("..")) {
clientWs.send(JSON.stringify({ type: "session_detail", error: "Ungueltiger Pfad" }));
return;
}
try {
// Letzte 100 Zeilen der Session (JSONL)
const raw = await dockerExec("aria-core", `tail -100 '${sessionPath.replace(/'/g, "")}'`);
const messages = [];
for (const line of raw.split("\n")) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
messages.push(obj);
} catch {}
}
clientWs.send(JSON.stringify({ type: "session_detail", path: sessionPath, messages, raw: messages.length === 0 ? raw : undefined }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "session_detail", error: err.message }));
}
}
async function handleDeleteSession(clientWs, sessionPath) {
if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith("/home/node/.openclaw/")) {
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
return;
}
try {
log("warn", "server", `Loesche Session: ${sessionPath}`);
await dockerExec("aria-core", `rm '${sessionPath.replace(/'/g, "")}'`);
clientWs.send(JSON.stringify({ type: "session_deleted", ok: true, path: sessionPath }));
log("info", "server", "Session geloescht");
} catch (err) {
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: err.message }));
}
}
// ── Brain Viewer ────────────────────────────────────────
async function handleListBrain(clientWs) {
try {
log("info", "server", "Lade Brain-Dateien...");
const raw = await dockerExec("aria-core", `
for f in /home/node/.openclaw/workspace/memory/*; do
[ -f "$f" ] || continue
name=$(basename "$f")
size=$(du -h "$f" 2>/dev/null | cut -f1)
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0)
# Frontmatter extrahieren (erste 10 Zeilen)
head10=$(head -10 "$f" 2>/dev/null | tr '\\n' '|')
echo "FILE:$name|SIZE:$size|LINES:$lines|MODIFIED:$modified|HEAD:$head10"
done
`.trim());
const files = [];
for (const line of raw.split("\n")) {
if (!line.startsWith("FILE:")) continue;
const parts = {};
for (const seg of line.split("|")) {
const idx = seg.indexOf(":");
if (idx > 0) {
const key = seg.slice(0, idx);
const val = seg.slice(idx + 1);
// HEAD hat mehrere |, also nur die bekannten Keys parsen
if (["FILE", "SIZE", "LINES", "MODIFIED"].includes(key)) {
parts[key] = val;
}
}
}
if (!parts.FILE || parts.FILE === "*") continue;
// Frontmatter-Info aus HEAD extrahieren
let description = "";
let memType = "";
const headPart = line.slice(line.indexOf("|HEAD:") + 6);
if (headPart) {
const headLines = headPart.split("|");
for (const hl of headLines) {
if (hl.startsWith("description:")) description = hl.replace("description:", "").trim();
if (hl.startsWith("type:")) memType = hl.replace("type:", "").trim();
}
}
files.push({
name: parts.FILE,
size: parts.SIZE || "?",
lines: parseInt(parts.LINES) || 0,
modified: parseInt(parts.MODIFIED) || 0,
description,
memType,
});
}
files.sort((a, b) => b.modified - a.modified);
clientWs.send(JSON.stringify({ type: "brain_list", files }));
log("info", "server", `${files.length} Brain-Datei(en) gefunden`);
} catch (err) {
log("error", "server", `Brain laden fehlgeschlagen: ${err.message}`);
clientWs.send(JSON.stringify({ type: "brain_list", files: [], error: err.message }));
}
}
async function handleReadBrainFile(clientWs, filename) {
// Path Traversal verhindern
if (!filename || filename.includes("..") || filename.includes("/")) {
clientWs.send(JSON.stringify({ type: "brain_content", error: "Ungueltiger Dateiname" }));
return;
}
try {
const content = await dockerExec("aria-core",
`cat '/home/node/.openclaw/workspace/memory/${filename.replace(/'/g, "")}'`);
clientWs.send(JSON.stringify({ type: "brain_content", filename, content }));
} catch (err) {
clientWs.send(JSON.stringify({ type: "brain_content", filename, error: err.message }));
}
}
// ── Start ───────────────────────────────────────────────
server.listen(HTTP_PORT, "0.0.0.0", () => {