Hermes „vergaß“ 798 Nachrichten nach einem Absturz — der Session-Amnesia-Bug, erklärt und behoben


Montagmorgen. Du öffnest Telegram, um den Faden wieder aufzunehmen, den du gestern Nacht mit Hermes zurückgelassen hast – und es beginnt Smalltalk über ein Thema, das vor drei Tagen hätte abgeschlossen sein sollen. „Wurde der PR gemerged?“

Das ist weder ein verwirrtes Modell noch deine Einbildung. Genau das ist Teknium, dem Lead hinter Hermes Agent, Anfang August 2026 passiert: Nach einem gateway-Absturz und -Neustart nahm sein Telegram-DM stillschweigend einen 2,7 Tage alten Kontext wieder auf – und die 798 echten Nachrichten, die dazwischen ausgetauscht wurden, schienen verschwunden. Nichts war verloren – Hermes konnte sie nur nicht finden.

Was passiert ist: Wie eine Nachricht eine Session in die Vergangenheit schickte

Das offizielle Tracking-Issue (#82616) enthält Beweise aus der Produktionsdatenbank. Der Vorfall lief in vier Schritten ab:

Wann Was passiert ist
6. Aug, 16:18 Eine eingehende Nachricht löst einen Recovery-Pfad aus, der stillschweigend eine neue Session-Zeile ohne Identität anlegt
6. Aug → 8. Aug Alle 798 echten Nachrichten landen in dieser Orphan-Zeile; das In-Memory-Mapping lässt das Gespräch völlig normal aussehen – der Fork ist unsichtbar
8. Aug, 17:03 Der gateway stürzt ab und startet neu; das In-Memory-Mapping „Chat → Session“ wird als veraltet verworfen
9. Aug, 09:40 Die nächste Nachricht trifft ein: Hermes löst den Chat per Session-Key auf, und die einzige Zeile mit dem Key ist die veraltete Session vom 3. Aug – also wird ein drei Tage alter Kontext wieder aufgenommen und über einen PR geplaudert, der schon Tage zuvor gemerged wurde

Die Datenbank-Belege zeigen zwei Zeilen: das „Original“ vom 3. Aug (voller Session-Key, aber ended_at leer und keine neuen Nachrichten) und die Orphan-Session vom 6. Aug (session_key, chat_id alles NULL – aber alle 798 Nachrichten hängen an ihr).

In einfachen Worten: Hermes hat stillschweigend eine namenlose Kopie der Session „geforkt“, jede Nachricht landete in der Kopie, ein Neustart wischte das In-Memory-Adressbuch weg, und als Hermes die Session wieder suchte, erkannte es nur die Zeile mit Namensschild – also ging es in der Zeit zurück.

Grundursache: Drei Fehler stapelten sich – und alle blieben still

Die Tiefenanalyse (vollständige Code-Analyse in den Kommentaren des Issues) lief auf eine einzige Kette hinaus:

  1. Die DB-Schreibvorgänge der Session-Rotation schlugen fehl – und die Fehler wurden verschluckt. Der Idle-Auto-Reset von Hermes beendet die alte Session und legt eine neue an – beide Schreibvorgänge scheiterten: einer wurde auf Debug-Ebene geloggt, der andere war ein nacktes print auf die Konsole. Niemand hat es bemerkt.
  2. Trotzdem war die Routing-Tabelle bereits auf die neue Session umgestellt. Die alte Session wurde zum „Zombie“ (nie als beendet markiert, hielt weiter den Session-Key), während die neue Session auf der Platte überhaupt nicht existierte.
  3. Ein Lazy-Writer materialisierte die Geisterzeile. Ein späterer Hintergrund-Schreibvorgang (z. B. die Token-Verbrauchsabrechnung) stellte fest, dass die Zielzeile fehlte, und legte sie spontan an – aber mit jedem Identitätsfeld (session_key, chat_id, …) leer. Die Orphan-Session war geboren, und kein Codepfad stempelt je wieder Identität darauf.
  4. Nach dem Absturz konnte die Neustart-Auflösung die Orphan-Session nicht sehen. Sie sucht Sessions per Key und per Chat-Infos – die Orphan-Session passt auf keins von beiden, also bleibt nur der Zombie „Original“ als Kandidat. Er gewinnt, und der alte Kontext kehrt zurück.

Es gibt eine faszinierende Wendung: Die Logs meldeten immer wieder possible FTS write corruption und disk=0 (0 Zeilen vom Datenträger zurückgelesen), sodass zunächst alle auf Datenbankkorruption tippten. Die Untersuchung bewies: Der Lesepfad fasst den Volltext-Index nie an – das eigentliche Problem war das Session-ID-Routing: Der Writer folgte einer Umleitungs-Tabelle, der Reader fragte die alte ID ab und bekam 0 Zeilen. Die Datenbank war die ganze Zeit gesund – genau deshalb ging keine einzige Nachricht verloren.

Nichts ging verloren: Die 798 Nachrichten leben in einer „versteckten“ Zeile

Der beruhigendste Teil des ganzen Vorfalls: Alle 798 Nachrichten sind intakt – sie sitzen nur in einer Zeile ohne Identitäts-Tag, unsichtbar für den Resolver. Für betroffene Nutzer dokumentiert das Issue eine manuelle Wiederherstellungsprozedur (weiter unten).

Der Fix: Prävention + Behandlung – am 2026-08-09 in main gemerged

Der Fix kommt in zwei PRs – einer, damit es nie wieder passiert, einer für den Schaden, der bereits existiert:

#82633 – Härtung auf der Schreibseite (Prävention)

  • Identität wird atomar geschrieben: Session-Key, Chat-ID und Ursprung sind jetzt Teil des INSERT selbst, das die Zeile anlegt (ON CONFLICT füllt per COALESCE nach), statt eines Best-Effort-UPDATE nach dem Anlegen. Identität und Zeile leben oder sterben jetzt gemeinsam.
  • Jeder Refresh ist eine Reparatur-Gelegenheit: Der gateway aktualisiert die Session-Infos in jedem Turn; fehlt eine Zeile, wird jetzt die volle Identität eingefügt, statt stillschweigend nichts zu tun.
  • Recency-bewusste Auflösung: Nach einem Absturz/Neustart werden Kandidaten-Sessions nach last_activity_at sortiert, Zeilen mit Nachrichten zuerst; eine frische ID wird nie vergeben, solange eine Zeile mit Key existiert.
  • Fehler sind nicht mehr still: Fehlgeschlagene End-/Create-Schreibvorgänge werden auf WARNING-Ebene geloggt, mit ausformulierter Routing-Konsequenz; Ausnahmen beim Transkript-Lesen liefern nicht mehr stillschweigend eine leere Liste zurück.
  • Behebt außerdem #12857: Session-Resets verlieren nicht mehr die Parent-Session-ID (Lineage).

Die Validierung war hart: 11 Regressionstests gegen das ungepatchte main produzierten 6 Fehlschläge; nach dem Fix bestehen alle. Eine End-to-End-Vorfallswiederholung (3-Tage-Zombie + 798-Nachrichten-Orphan auf einer echten Datenbank) führt zurück zur Live-Konversation.

#82712 – hermes sessions repair-routing (Behandlung)

Ein neuer Befehl, der verwaiste Sessions „rettet“, entworfen nach dem Prinzip Fail-Closed:

hermes sessions repair-routing              # dry-run first: report findings, change nothing
hermes sessions repair-routing --apply      # actually repair, after confirmation
  • Erkennung: Scannt gateway-Session-Zeilen ohne Key, aber mit echten Nachrichten (Branch-/Delegate-/Tool-Zeilen sind designbedingt ohne Key und werden ausgeschlossen – keine False Positives).
  • Belegbasiert: Er handelt nur, wenn die Belege eindeutig sind – entweder aufgezeichnete Lineage (parent_session_id, die auf eine mit Key versehene Zeile derselben Quelle zeigt) oder genau ein Vorgänger mit Key, der innerhalb von 15 Minuten nach dem Start der Orphan-Session verstummte. Zwei Kandidaten-Vorgänger oder zwei Orphan-Sessions, die denselben Vorgänger beanspruchen, werden mit Begründung abgelehnt – eine Fehladoption würde das Gespräch einer Person in den Chat einer anderen Person einfügen, also weigert sich der Befehl zu raten.
  • Die Reparatur: Stempelt die Orphan-Session per COALESCE mit der Identität des Vorgängers (überschreibt nie einen vorhandenen Wert), zeichnet die Lineage auf und zieht den Vorgänger mit end_reason='superseded_by_repair' aus dem Verkehr – bewusst nicht der normale Reset-Grund, damit der nächste Neustart nicht zurückdriftet.

Bist du betroffen?

Prüfe das Profil, bevor du in Panik gerätst:

  • Nur gateway-Sessions: Telegram-, Discord-, Slack-, WhatsApp- und andere Messaging-Plattform-Sessions. Reine CLI-Nutzer sind strukturell immun – die CLI hat keine dieser Session-Routing-Mechanismen.
  • Höchstes Risiko: langlebige Sessions, alle, die den gateway neu starten/aktualisieren/abstürzen lassen, und stark multimodale Konversationen.
  • Kein Einzelfall: Dieselbe Installation zeigt 5 Orphan-Session-Vorfälle seit Juni (42, 34, 5, 798 und 2 Nachrichten) – das hier war nur der größte, und er traf den Lead-Entwickler.
  • Typisches Symptom: Nach einem gateway-Neustart redet Hermes über alte Themen, erinnert sich nicht an die letzten Tage und bezieht sich auf längst Abgeschlossenes.

Aktualisieren und Daten wiederherstellen

Wichtig: Der Fix liegt auf main (gemerged am 2026-08-09), ist aber noch in keinem release enthalten – das neueste release ist weiterhin v0.20.0 (2026-08-03). Langzeitnutzern wird empfohlen zu aktualisieren, und zwar über einen von zwei Wegen:

  1. Auf das nächste release warten, oder
  2. Von main aus laufen für sofortige Abdeckung (siehe Installationsanleitung und Update-Befehlsreferenz).

Bereits betroffen und willst dein Gespräch zurück:

  • Bevorzugt: Auf einen Build mit dem Fix aktualisieren, dann hermes sessions repair-routing ausführen (zuerst dry-run, dann --apply).
  • Manuelle Prozedur (aus dem offiziellen Issue, falls das Tool nicht verfügbar ist):
    1. Den gateway stoppen;
    2. Zuerst ein Backup: cp state.db state.db.bak;
    3. session_key, chat_id, chat_type, origin_json usw. der veralteten „Original“-Zeile auf die Orphan-Zeile kopieren (nur Leerstellen füllen, nie überschreiben);
    4. ended_at und end_reason='superseded_by_repair' auf der veralteten Zeile setzen;
    5. Den gateway neu starten und die Auflösung die echte Session finden lassen.

Sichere state.db, bevor du manuelle Schritte ausführst, und stelle sicher, dass der gateway vollständig gestoppt ist. Im Zweifel warte auf das Fix-release und nutze repair-routing – das ist sicherer als manuelles Editieren.

Was dieser Vorfall uns lehrt

Der gruseligste Teil ist nicht der Bug selbst – es ist die Stille. Der Session-Fork passierte leise im Hintergrund; die In-Memory-Kontinuität hielt die Illusion aufrecht, dass alles in Ordnung sei – bis ein Neustart es offenlegte. Zwei Erkenntnisse für alle, die einen langlebigen KI-Assistenten betreiben: erstens nach Neustarts und Updates die Session-Kontinuität prüfen; zweitens Hermes-Daten verschwinden selten wirklich – eine „verlorene“ Konversation ist meist ein Routing-Problem, kein Datenproblem.

Wer tiefer in die Session-Verwaltung einsteigen will, findet Details in der hermes sessions-Befehlsreferenz; für das neueste Haupt-release gibt es die v0.20.0-Release-Notes.