Mehr als die Hälfte dieser state.db waren Kopien von Nachrichten, die du längst gepruned hattest

Der Lüfter deines Laptops springt inzwischen auch bei ruhiger Arbeit an. Backups dauern jede Woche länger. hermes sessions stats meldet eine Datenbank von mehreren Gigabyte — aber deine Session-Liste sieht nicht nach mehreren Gigabyte aus. Die meisten Gespräche sind ein Dutzend Turns, ihre Historie wurde längst komprimiert. Wo ist der Platz also hin?
Die Antwort ist etwas kontraintuitiv: Er steckt in den alten Nachrichten, die du bereits gepruned hast. Ihre vollständigen Bodies wurden immer wieder in „archivierte“ Zwillinge kopiert. Auf einer Installation mit aktiviertem optionalen Prune sammelte ein einzelnes Profil 31.930 dieser Kopien an; ein zweites Profil hatte 48.737 — 55 % aller seiner Rows.
Was Prune tatsächlich tut
Hermes hat ein Opt-in-Feature namens proaktiver Prune (compression.proactive_prune_tokens, standardmäßig aus). Es ist bewusst bescheiden: Wenn die erneut gesendete Historie deinen Token-Schwellwert überschreitet, ersetzt es alte, sperrige Tool-Ergebnisse — Terminal-Dumps, File-Reads, Web-Extracts — durch kurze Zusammenfassungen. Kein Modellaufruf beteiligt.
Der wichtige Teil: Ein Prune bearbeitet den Body einiger bestehender Nachrichten. Er fügt nie eine Nachricht hinzu, löscht keine und sortiert nicht um. Aus Sicht der Daten ist es ein lokales Edit.
Der alte Commit-Pfad sah das anders. Jeder Commit lief durch den vollständigen Archive-and-Compact-Writer: jede aktive Row archivieren, dann die ganze Liste neu einfügen.
Du änderst drei Nachrichten; die Datenbank bekommt eine Kopie des gesamten Transcripts.
Einmal oder zweimal ist das nichts. Aber Prune feuert in einer langen Session wiederholt, und die Kopien stapeln sich: Jede ist „die vollständige Historie, wie sie damals aussah“, und ein Großteil dieses Inhalts wird zum Ausgangsmaterial der nächsten Kopie. Deshalb wächst die Datei viel schneller als deine Gespräche.
Die gemessenen Zahlen
Keine Schätzungen — sie stammen aus dem Tracking-Issue:
| Beobachtung | Wert |
|---|---|
| Abgelaufene Kopien, erstes Profil | 31.930 Rows |
| Abgelaufene Kopien, zweites Profil | 48.737 Rows |
| Anteil an den Rows dieses Profils | 55 % |
| Auslöser | proaktiver Prune aktiviert (Beispiel-Config proactive_prune_tokens: 12000) |
Die Hälfte dieser Datenbank tat nichts anderes, als Reads, Writes, Backups und den Volltextindex langsamer zu machen.
Was sich geändert hat: Rewrite an Ort und Stelle
Das neue Commit-Primitive ist SessionDB.rewrite_pruned_rows(session_id, changes, model_config_patch=...) und läuft in einer einzigen Transaktion:
- Die Row benennen. Zuerst über
_row_idauflösen; sonst übertool_call_id(Tool-Ergebnisse) oder die Liste der Tool-Call-IDs, die ein Assistant-Turn trägt — innerhalb einer Session eindeutig; andernfalls nur die einzelne aktive Row mit derselben Rolle und demselben gespeicherten Inhalt. - Beweisen, dass sie noch das Original hält. Gleiche Rolle, gleiche Tool-Call-ID, gleiche Call-IDs und bei Tool-Rows derselbe gespeicherte Body. Jede Abweichung bricht den In-Place-Pfad ab.
- Einen archivierten Zwilling des Zustands vor dem Prune stehen lassen (
active=0, compacted=1), damit der herabgestufte volle Body genau wie vorher durchsuchbar bleibt. Compaction heißt weiterhin nicht Datenverlust. - Die geänderten Spalten unter ihrer eigenen ID übernehmen und den Rearm-Patch in
model_configmergen.
Unveränderte Rows, Modellreihenfolge, Row-IDs und parallel angehängte Rows werden nie angefasst. Die bestehenden Trigger messages_fts_update und messages_display_identity_update halten Volltext- und Display-Index konsistent, und message_count bleibt unverändert, weil keine Row in die Live-Menge eintritt oder sie verlässt.
Ein Detail, dem man trauen kann: Wenn die Row nicht exakt benannt werden kann — gemergte _absorbed_row_ids, null oder mehrere Treffer, eine inaktive Row, eine User-Row, ein Body, der sich bereits geändert hat —, dann rät das Primitive nicht. Es wirft PruneRowUnresolvedError vor jedem Write, loggt das auf INFO und fällt auf den alten Archive-and-Compact-Pfad zurück. Langsamer, aber korrekt.
So nutzt du es und prüfst deinen eigenen Store
1. Herausfinden, ob Prune an ist.
hermes config get compression.proactive_prune_tokens
0 (oder „Config key not set“) heißt aus, und dieser Mechanismus hat deine Datenbank dann nicht wachsen lassen. Zum Aktivieren liegen alle drei Keys unter compression::
compression:
proactive_prune_tokens: 48000 # 0 = aus; 48000 ist ein vernünftiger Start
proactive_prune_min_result_chars: 8000 # kleinere Ergebnisse bleiben unangetastet
proactive_prune_min_reclaim_tokens: 4096 # nicht committen, wenn weniger zurückgewonnen wird
2. Den Store messen.
hermes sessions stats
du -sh ~/.hermes/state.db
(Nicht-Standard-Profile halten ihre Daten unter ihrem eigenen Profil-Verzeichnis.)
3. Platz zurückholen — zwei verschiedene Operationen, nicht verwechseln.
hermes sessions optimize # FTS5-Segmente mergen + VACUUM; keine Session-Daten ändern sich
hermes sessions optimize-storage # den Volltextindex auf das kompakte v23-Layout migrieren
optimize holt freie Pages zurück; optimize-storage ändert das Index-Layout und kann bei großen Datenbanken einen großen Anteil zurückgewinnen. Keine der beiden löscht alte Kopien für dich — was archiviert oder gelöscht wird, bleibt deine Retention-Entscheidung. Der Fix verhindert neue Kopien; er räumt die bestehenden nicht auf. Vorhandene Rows bleiben, bis du Sessions nach deinem eigenen Zeitplan archivierst oder löschst.
Ein ehrlicher Status-Hinweis: Die Änderung wurde am 18. Oktober 2026 nach main gemergt, nachdem v0.22.2 am 15. Oktober erschienen ist, das aktuelle Release enthält sie also nicht. Neue Prunes nutzen den In-Place-Pfad ab deinem nächsten hermes update.
Warum diese Art von Wachstum unsichtbar bleibt
Drei Gründe kommen zusammen.
- Es ist ein Nebenprodukt von etwas Korrektem. Archive-and-Reinsert garantierte, dass jeder Commit ein vollständiger, konsistenter Snapshot war. Es gab keinen Logikfehler — die Kosten wurden nur auf die Festplatte gebucht.
- Metriken zeigen es nicht. Session-Anzahl unverändert, Message-Count unverändert, UI in Ordnung. Nur Dateigröße und I/O werden langsam schlechter.
- Es skaliert mit der Nutzung. Je mehr lange Sessions und große Tool-Ausgaben du fährst, desto schneller wächst es — und gerade Vielnutzer merken es am wenigsten, weil ihre Maschinen ohnehin beschäftigt sind.
Das ist keine Datenbank-Korruption, und beides sollte man nicht vermischen: Ein kaputtes Schema oder eine verschwundene Session hat ihren eigenen Reparaturpfad, beschrieben in eine beschädigte state.db wiederherstellen. Hier waren die Daten völlig gesund — es war einfach zu viel davon.
Die allgemeine Lehre
„An Ort und Stelle aktualisieren“ und „alles neu schreiben“ sind funktional äquivalent und kostenmäßig völlig verschieden. Viele Systeme wählen den Rewrite, weil er einfach ist und keinen halb geschriebenen Zustand hinterlassen kann; der Preis ist, bei jedem kleinen Edit alle Daten zu kopieren. Leg das auf einen Hot Path — alle paar Turns —, und der Preis summiert sich schneller, als du erwartest.
Der Test ist grob: Wenn du eine Row geändert hast, solltest du dann N schreiben? Wenn nicht, wird dieser Pfad irgendwann zum Kapazitätsproblem. Der Fix braucht selten eine neue Storage-Engine; meist braucht er nur, dass der Aufrufer ein In-Place-Update erkennt — hier: die exakte Row über ihre Tool-Call-Identität benennen und prüfen, dass sich sonst nichts bewegt hat.
Um Kontext- und Historien-Größe bewusster zu steuern, gehen zwei verwandte Beiträge tiefer: die Lean-Tail-Compression-Strategie behandelt, was eine Compaction übersteht, und den Prompt nach der Compaction neu aufbauen behandelt, was das Modell danach sieht. Wenn du Sessions aus der Shell verwaltest, listet die hermes-sessions-Referenz jedes Subkommando und jeden Filter.
Zusammenfassung
- Symptom:
state.dbweit größer als dein Gesprächsvolumen; langsame Backups, viel I/O, merkwürdige Row-Zahlen inhermes sessions stats. - Ursache: Jeder proaktive Prune-Commit archivierte alle aktiven Rows und fügte die ganze Liste neu ein — 48.737 überholte Kopien in einem Profil, 55 % seiner Rows.
- Fix:
rewrite_pruned_rows()aktualisiert nur die geänderten Spalten, behält einen durchsuchbaren archivierten Zwilling und fällt sicher zurück, wenn eine Row nicht exakt benannt werden kann. - Heute: den Schalter mit
hermes config get compression.proactive_prune_tokensprüfen, den Store mithermes sessions statsbemessen, mithermes sessions optimizezurückholen und updaten, um den neuen Commit-Pfad zu bekommen.