diff --git a/aria-data/config/BOOTSTRAP.md b/aria-data/config/BOOTSTRAP.md index b546ca3..4c278b9 100644 --- a/aria-data/config/BOOTSTRAP.md +++ b/aria-data/config/BOOTSTRAP.md @@ -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 diff --git a/diagnostic/index.html b/diagnostic/index.html index fc3811b..06518b1 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -160,6 +160,43 @@ + +
+
+
+

Sessions

+ +
+
+ +
+
+
+

Brain / Memory

+ +
+
+ + +
+
+
@@ -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 = '
Lade...
'; + send({ action: 'list_sessions' }); + } + + function renderSessions(data) { + const container = document.getElementById('sessions-list'); + if (data.error) { + container.innerHTML = `
Fehler: ${escapeHtml(data.error)}
`; + return; + } + if (!data.sessions || data.sessions.length === 0) { + container.innerHTML = data.raw + ? `
${escapeHtml(data.raw)}
` + : '
Keine Sessions gefunden
'; + return; + } + let html = ''; + html += '' + + '' + + '' + + '' + + '' + + ''; + 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 += `` + + `` + + `` + + `` + + `` + + `` + + ''; + } + html += '
SessionZeilenGroesseZuletzt
${key}${s.lines}${escapeHtml(s.size)}${date}
'; + 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 = '
Lade...
'; + send({ action: 'read_session', sessionPath: path }); + } + + function renderSessionDetail(data) { + const content = document.getElementById('session-detail-content'); + if (data.error) { + content.innerHTML = `
${escapeHtml(data.error)}
`; + return; + } + if (data.raw) { + content.innerHTML = `
${escapeHtml(data.raw)}
`; + return; + } + if (!data.messages || data.messages.length === 0) { + content.innerHTML = '
Keine Nachrichten
'; + 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 += `
` + + `${escapeHtml(role)} ` + + `${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''}` + + '
'; + } + 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 = '
Lade...
'; + 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 = `
Fehler: ${escapeHtml(data.error)}
`; + 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 += `
` + + `` + + `
` + + `
${escapeHtml(f.name)}
` + + (f.description ? `
${escapeHtml(f.description)}
` : '') + + `
` + + `
${escapeHtml(f.size)}
` + + '
'; + } + container.innerHTML = html || '
Nur .gitkeep gefunden
'; + } + + 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(); diff --git a/diagnostic/server.js b/diagnostic/server.js index d5f73ee..2aaf689 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -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//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", () => {