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 = '
'
+ '| Session | '
+ 'Msgs | '
+ 'Zuletzt | '
+ ' |
';
- 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} ${modelBadge} | `
+ + `${key}${activeBadge}${orphanBadge}${archivedBadge}
${modelBadge}`
+ `${s.lines} | `
+ `${date} | `
- + ``
- + (isActive ? '' : ``)
- + ``
- + ``
- + ` |
`;
- }
+ + `${actions} | `;
+ };
+
+ let html = '' + 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);
+}