Die Session, die du gelöscht hast, kam zurück: zwei Bugs und eine neue Sperre

Du löschst eine Session, die in der Desktop-App aus dem Ruder gelaufen ist, aktualisierst die Liste — und da ist sie wieder. Oder du löschst eine über die CLI und merkst danach, dass die Session nie wieder eine Antwort speichert: Der Agent antwortet weiter, die Historie bleibt leer, und die einzige Spur ist eine einzelne Zeile im Log — Session DB append_message failed: FOREIGN KEY constraint failed. Beide Geschichten sind real, und beide stammen aus demselben Paket von Fixes.
PR #125157, gemergt am 2026-09-27, ändert das Verhalten selbst: Solange ein laufender Turn eine Session besitzt, wird ein Löschen verweigert, statt still rückgängig gemacht zu werden.
Zuerst zwei echte Bugs
Issue #123583 dokumentierte den ersten ausführlich: Die Session-Zeile verschwindet aus dem Store, während das Agent-Objekt, das sie besitzt, noch lebt. Sein internes Flag _session_db_created glaubt weiterhin, die Zeile existiere, also ruft der Flush einmal append_message auf, der Foreign Key lehnt es ab, und der Fehler wird nur geloggt — kein Retry und danach nichts Beobachtbares, denn ein Rollback ist ein Rollback. Das wiederholt sich bei jedem Turn. Der Chat sieht dabei völlig normal aus, während dein dauerhaftes Transkript aufhört zu wachsen.
Der zweite ist dessen Fix (PR #124117, gerettet aus #123641): Sobald eine Zeile bestätigt fehlt, erstellt der nächste Flush die Session unter derselben ID neu und spielt das gesamte Transkript-Präfix erneut ein, nicht nur den neuen Turn. Der erste Versuch speicherte nur den neuen Turn, weil die History-Dicts „schon gespeichert“ aussahen — die Art von Detail, die im Rückblick immer offensichtlich ist. Der Fix besteht außerdem darauf, dass nur eine bestätigt fehlende Zeile geheilt wird; andere Schreibfehler lösen keinen Rebuild aus.
Die neue Sperre: das Löschen verweigern, nicht rückgängig machen
| Oberfläche | Session von einem laufenden Turn gehalten | Idle / durch Compaction geschlossen |
|---|---|---|
CLI sessions delete, export --delete-after-verified |
Verweigert, Exit-Code 1, Cannot delete active session |
Gelöscht |
| Desktop, Einzellöschung | 409 | Gelöscht |
| Desktop, Bulk-Löschung | Live-Wurzeln übersprungen und in skipped_active gemeldet, mit einem Toast, der beide Zahlen zeigt |
Gelöscht |
API-Server DELETE |
409 | Gelöscht |
TUI session.delete |
4023 (der bestehende Code für „kann keine aktive Session löschen“) | Gelöscht |
sessions prune |
Live-Zeilen verschont (unverändert) | Gepruned |
Ein paar Implementierungsdetails sind es wert, sie zu kennen:
- Die Sperre wird innerhalb der eigenen Schreibtransaktion des Löschens geprüft (derselbe Pfad
_write_guards_reject/_guarded_ids, den prune bereits nutzte), sodass sich zwischen Prüfung und Löschen nichts ändern kann. - Ist eine Wurzel-Session idle, hält aber ein Delegate-Kind noch ein Live-Lease, wird das Löschen ebenfalls verweigert — das Löschen würde in dieses Kind kaskadieren.
- Sessions, die beendet wurden (zum Beispiel durch Compaction geschlossen), ohne dass ein Turn sie besitzt, sind weiterhin löschbar.
- Die Bulk-Löschung macht eine gebündelte Sperrprüfung und fällt nur dann auf Prüfungen pro Wurzel zurück, wenn tatsächlich etwas gesperrt ist.
Was du sehen wirst
hermes sessions list # Ziel finden
hermes sessions delete 20260928_101500_ab12cd # live -> Cannot delete active session, Exit 1
hermes sessions export 20260928_101500_ab12cd --delete-after-verified # exportieren und verifizieren, dann löschen
hermes sessions prune --older-than 30d # Live-Zeilen werden immer verschont
In der TUI liefert ein Live-Löschen 4023; auf dem Desktop oder über die API ist es ein 409. In der Praxis ist der einfachste Weg, den Turn zu Ende laufen zu lassen — oder ihn per /stop zu stoppen — und danach zu löschen.
Ist der „Comeback“-Fall also noch ein Problem?
Er ist abgedeckt, und zwar von beiden Seiten. Der Rebuild-Fix (#124117) deckt „die Zeile ist weg, die Konversation aber nicht“ ab: Die Session kehrt beim nächsten erfolgreichen Write mit ihrem vollständigen History-Präfix zurück, sodass ein Löschen mitten im Flug dich nicht mehr die folgenden Turns kostet. Die Sperre (#125157) deckt die andere Seite ab: Ein Löschen findet entweder statt oder sagt dir, warum — ohne Zwischenzustand, in dem die UI die Session als verschwunden zeigt und das Backend weiter schreibt.
Wenn du vermutest, dass du bereits Rückstände aus dieser Bug-Klasse hast, schau dir die eigenen Statistiken des Stores an, bevor du etwas Drastisches tust. Session-Grenzen erklärt, wann Sessions geteilt und wann sie beendet werden, der state.db-Beitrag behandelt die Wiederherstellung auf Datenbankebene, und die genauen Flags stehen in unserer hermes sessions Referenz.
Release-Status
Alle drei Änderungen wurden am 2026-09-27 gemergt, nach dem v0.21.5-Release-Tag (v2026.9.24), liegen also nur auf main: hermes update holt sie.
Das Fazit
„Die Session kam zurück“ und „die Session ist weg, empfängt aber weiterhin Turns“ sehen wie zwei verschiedene Geschichten aus, sind aber eine: Das Löschen passte nie zu dem Turn, der die Session gerade benutzte. Der Fix macht das explizit — verweigern, solange der Turn läuft, und dem Nutzer sagen, warum; eine wirklich verlorene Zeile beim nächsten Write neu aufbauen. Die einzige Gewohnheit, die es zu ändern lohnt, ist klein: Bevor du eine Session löschst, stelle sicher, dass ihr Turn tatsächlich beendet ist.