feat: Archivierte Session-Versionen (OpenClaw .reset.* Files) in Diagnostic

OpenClaw resettet Sessions beim ersten chat.send nach Container-Restart
(wenn abortedLastRun / systemSent Inkonsistenz erkannt wurde) und
benennt die alte .jsonl in .jsonl.reset.<timestamp>.Z um. Der Inhalt
war also gar nicht verloren, nur unsichtbar.

Diagnostic:
- handleListSessions scannt jetzt auch *.jsonl.reset.* Files
- Reset-Files bekommen archived:true + resetAt-Timestamp
- Neue UI-Sektion "Archivierte Versionen" (collapsible <details>)
  mit Export-Button, zeigt aufklappbar alle gesicherten alten Sessions
- Aktivieren ist fuer Archive deaktiviert (zerstoert aktive Session)
- Loeschen + Export stehen zur Verfuegung

tools/export-jsonl-to-md.js:
- Standalone Node-Script zum Konvertieren beliebiger .jsonl (auch reset-Files)
- Nutzbar via stdin, exakt gleiche Export-Logik wie Diagnostic
- Fuer Rettungsaktionen direkt auf der VM

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 12:24:54 +02:00
parent 87deede078
commit 76d72a1eef
4 changed files with 141 additions and 18 deletions
+41 -14
View File
@@ -1703,33 +1703,60 @@
: '<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;">'
const active = data.sessions.filter(s => !s.archived);
const archives = data.sessions.filter(s => s.archived);
const headerRow = '<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;">Msgs</th>'
+ '<th style="padding:4px 6px;">Zuletzt</th>'
+ '<th style="padding:4px 6px;"></th></tr>';
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 ? ' <span style="background:#FF3B30;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">verwaist</span>' : '';
const archivedBadge = s.archived ? ' <span style="background:#555570;color:#fff;font-size:9px;padding:1px 4px;border-radius:3px;">archiv</span>' : '';
const modelBadge = s.model ? `<div style="font-size:9px;color:#555570;">${escapeHtml(s.model)}</div>` : '';
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 ? ' <span style="background:#34C759;color:#000;font-size:9px;padding:1px 4px;border-radius:3px;">aktiv</span>' : '';
const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : '';
html += `<tr style="border-bottom:1px solid #0D0D1A;cursor:pointer;${rowBg}" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background='${isActive ? 'rgba(52,199,89,0.08)' : ''}'">`
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 = `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Archiv endgueltig loeschen">X</button>`
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">&#x2B07;</button>`;
} else {
actions = (isActive ? '' : `<button class="btn secondary" onclick="event.stopPropagation();activateSession('${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#34C759;margin-right:2px;" title="Aktivieren">&#9654;</button>`)
+ `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Loeschen">X</button>`
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">&#x2B07;</button>`;
}
return `<tr style="border-bottom:1px solid #0D0D1A;cursor:pointer;${rowBg}" onmouseover="this.style.background='#1E1E2E'" onmouseout="this.style.background='${isActive ? 'rgba(52,199,89,0.08)' : (s.archived ? 'rgba(136,136,170,0.04)' : '')}'">`
+ `<td style="padding:4px 6px;" onclick="viewSession('${escapeHtml(s.path)}')">`
+ `<div style="color:${keyColor};">${key}${activeBadge}${orphanBadge}</div>${modelBadge}</td>`
+ `<div style="color:${keyColor};">${key}${activeBadge}${orphanBadge}${archivedBadge}</div>${modelBadge}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;">${s.lines}</td>`
+ `<td style="padding:4px 6px;color:#8888AA;font-size:10px;">${date}</td>`
+ `<td style="padding:4px 6px;white-space:nowrap;">`
+ (isActive ? '' : `<button class="btn secondary" onclick="event.stopPropagation();activateSession('${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#34C759;margin-right:2px;" title="Aktivieren">&#9654;</button>`)
+ `<button class="btn secondary" onclick="event.stopPropagation();deleteSession('${escapeHtml(s.path)}')" style="padding:2px 6px;font-size:10px;color:#FF6B6B;margin-right:2px;" title="Loeschen">X</button>`
+ `<button class="btn secondary" onclick="event.stopPropagation();exportSession('${escapeHtml(s.path)}','${escapeHtml(s.sessionKey)}')" style="padding:2px 6px;font-size:10px;color:#8888AA;" title="Als Markdown exportieren">&#x2B07;</button>`
+ `</td></tr>`;
}
+ `<td style="padding:4px 6px;white-space:nowrap;">${actions}</td></tr>`;
};
let html = '<table style="width:100%;border-collapse:collapse;">' + headerRow;
for (const s of active) html += rowFor(s);
html += '</table>';
if (archives.length > 0) {
html += `<details style="margin-top:12px;" ${archives.length <= 5 ? 'open' : ''}>`
+ `<summary style="color:#8888AA;font-size:11px;cursor:pointer;padding:4px 0;">`
+ `Archivierte Versionen (${archives.length}) — von OpenClaw beim Session-Reset gesichert`
+ `</summary>`
+ `<table style="width:100%;border-collapse:collapse;margin-top:6px;">` + headerRow;
for (const s of archives) html += rowFor(s);
html += '</table></details>';
}
container.innerHTML = html;
}
+24 -4
View File
@@ -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.<ISO-Timestamp>.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}`,