diff --git a/diagnostic/index.html b/diagnostic/index.html index 139ebc4..bcb869c 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -1703,33 +1703,60 @@ : '
Keine Sessions gefunden
'; return; } - let html = ''; - html += '' + + const active = data.sessions.filter(s => !s.archived); + const archives = data.sessions.filter(s => s.archived); + + const headerRow = '' + '' + '' + '' + ''; - for (const s of data.sessions) { + + const rowFor = (s, opts) => { const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?'; const key = escapeHtml(s.sessionKey || s.path.split('/').pop()); const orphanBadge = s.orphan ? ' verwaist' : ''; + const archivedBadge = s.archived ? ' archiv' : ''; const modelBadge = s.model ? `
${escapeHtml(s.model)}
` : ''; - const isActive = (s.sessionKey === currentActiveSession); - const keyColor = isActive ? '#34C759' : (s.orphan ? '#555570' : '#E0E0F0'); + const isActive = (s.sessionKey === currentActiveSession) && !s.archived; + const keyColor = isActive ? '#34C759' : (s.archived || s.orphan ? '#8888AA' : '#E0E0F0'); const activeBadge = isActive ? ' aktiv' : ''; - const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : ''; - html += `` + const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : (s.archived ? 'background:rgba(136,136,170,0.04);' : ''); + + let actions = ''; + if (s.archived) { + // Archive: nur Export + Loeschen (kein Aktivieren — wuerde aktive Session ueberschreiben) + actions = `` + + ``; + } else { + actions = (isActive ? '' : ``) + + `` + + ``; + } + + return `` + `` + + `
${key}${activeBadge}${orphanBadge}${archivedBadge}
${modelBadge}` + `` + `` - + ``; - } + + ``; + }; + + let html = '
SessionMsgsZuletzt
` - + `
${key}${activeBadge}${orphanBadge}
${modelBadge}
${s.lines}${date}` - + (isActive ? '' : ``) - + `` - + `` - + `
${actions}
' + headerRow; + for (const s of active) html += rowFor(s); html += '
'; + + if (archives.length > 0) { + html += `
` + + `` + + `Archivierte Versionen (${archives.length}) — von OpenClaw beim Session-Reset gesichert` + + `` + + `` + headerRow; + for (const s of archives) html += rowFor(s); + html += '
'; + } + container.innerHTML = html; } diff --git a/diagnostic/server.js b/diagnostic/server.js index dbf69a2..dd577ad 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -1575,14 +1575,13 @@ async function handleListSessions(clientWs) { try { log("info", "server", "Lade Sessions aus aria-core..."); - // sessions.json als Index lesen + Datei-Details holen + // sessions.json als Index lesen + Datei-Details holen (inkl. .reset.* Archive) const raw = await dockerExec("aria-core", ` cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}' && echo '===FILE_DETAILS===' && - for f in ${SESSIONS_DIR}/*.jsonl; do + for f in ${SESSIONS_DIR}/*.jsonl ${SESSIONS_DIR}/*.jsonl.reset.*; do [ -f "$f" ] || continue name=$(basename "$f") - # Nur echte User/Assistant Messages zaehlen — nicht Tool-Calls, Events etc. msgs=$(grep -cE '"role":"(user|assistant)"' "$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) @@ -1641,8 +1640,29 @@ async function handleListSessions(clientWs) { delete fileDetails[filename]; } - // Dateien die nicht im Index stehen (Waisen / Reset-Files) + // Dateien die nicht im Index stehen (Waisen ODER Reset-Archive) for (const [filename, details] of Object.entries(fileDetails)) { + // .jsonl.reset..Z → archivierte Session (OpenClaw-Reset) + const resetMatch = filename.match(/^([a-f0-9-]+)\.jsonl\.reset\.(.+)\.Z$/); + if (resetMatch) { + const id = resetMatch[1]; + // Timestamp ISO-8601 parsen (in Dateinamen: : durch - ersetzt) + // z.B. 2026-04-18T09-49-44.814 → 2026-04-18T09:49:44.814Z + const tsStr = resetMatch[2].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3"); + const resetAt = Math.floor(new Date(tsStr + "Z").getTime() / 1000) || parseInt(details.MODIFIED) || 0; + sessions.push({ + path: `${SESSIONS_DIR}/${filename}`, + sessionKey: id.slice(0, 8) + "… (archiv)", + sessionId: id, + lines: parseInt(details.LINES) || 0, + size: details.SIZE || "?", + modified: resetAt, + archived: true, + resetAt, + }); + continue; + } + // Echte Waisen (UUID.jsonl ohne Eintrag in sessions.json) const id = filename.replace(".jsonl", ""); sessions.push({ path: `${SESSIONS_DIR}/${filename}`, diff --git a/issue.md b/issue.md index b9708b0..b43f281 100644 --- a/issue.md +++ b/issue.md @@ -39,6 +39,8 @@ - [x] App: Audio-Aufnahme explizit 16kHz mono (spart Resample, optimal fuer Whisper) - [x] Gespraechsmodus: Speech-Gate strenger (-28dB / 500ms) — keine Umgebungsgeraeusche mehr - [x] Gespraechsmodus: Max-Dauer 30s pro Aufnahme, Cache-Cleanup alter Files, Messages-Array gekappt (500) +- [x] Diagnostic: Archivierte Session-Versionen (.reset.*) werden angezeigt + exportierbar — OpenClaw resettet Sessions bei erster Nutzung nach Container-Restart, Inhalt ist aber in .reset. Dateien gesichert +- [x] tools/export-jsonl-to-md.js: CLI-Konverter fuer beliebige Session-JSONL zu Markdown ## Offen diff --git a/tools/export-jsonl-to-md.js b/tools/export-jsonl-to-md.js new file mode 100755 index 0000000..90f3219 --- /dev/null +++ b/tools/export-jsonl-to-md.js @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * Exportiert ein OpenClaw Session-JSONL (auch .reset.*) als Markdown. + * + * Nutzung: + * node export-jsonl-to-md.js [output.md] + * + * Oder direkt aus dem aria-core Container: + * docker exec aria-core cat /home/node/.openclaw/agents/main/sessions/.jsonl.reset. \ + * | node export-jsonl-to-md.js - > output.md + */ + +const fs = require("fs"); + +const inputArg = process.argv[2]; +const outputArg = process.argv[3]; + +if (!inputArg) { + console.error("Usage: export-jsonl-to-md.js [output.md]"); + process.exit(1); +} + +const raw = inputArg === "-" ? fs.readFileSync(0, "utf-8") : fs.readFileSync(inputArg, "utf-8"); +const lines = raw.split("\n").filter(l => l.trim()); + +const blocks = []; +for (const line of lines) { + let obj; + try { obj = JSON.parse(line); } catch { continue; } + if (obj.type !== "message" || !obj.message) continue; + const role = obj.message.role; + if (role !== "user" && role !== "assistant") continue; + + let text = ""; + const content = obj.message.content; + if (typeof content === "string") text = content; + else if (Array.isArray(content)) text = content.filter(c => c.type === "text").map(c => c.text || "").join("\n"); + if (!text) continue; + + if (role === "user") { + text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim(); + text = text.replace(/^\[.*?\]\s*/, "").trim(); + } else { + text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim(); + } + if (!text) continue; + + const ts = obj.message.timestamp || obj.timestamp || 0; + const when = ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : ""; + const heading = role === "user" ? "## 🧑 User" : "## 🤖 ARIA"; + blocks.push(`${heading}${when ? ` — ${when}` : ""}\n\n${text}`); +} + +const exportedAt = new Date().toISOString().replace("T", " ").slice(0, 19); +const title = inputArg === "-" ? "Session" : inputArg.split("/").pop().replace(/\.jsonl.*/, ""); +const md = [ + `# Session: ${title}`, + ``, + `Exportiert: ${exportedAt} `, + `Quelle: ${inputArg === "-" ? "stdin" : inputArg}`, + `Nachrichten: ${blocks.length}`, + ``, + `---`, + ``, + blocks.join("\n\n---\n\n"), + ``, +].join("\n"); + +if (outputArg) { + fs.writeFileSync(outputArg, md); + console.error(`OK: ${blocks.length} Nachrichten → ${outputArg}`); +} else { + process.stdout.write(md); +}