Dein Plugin merkt jetzt endlich, wenn eine Session zusammengefasst oder gelöscht wird


Du lieferst ein Memory-Plugin aus, das die Coding-Konventionen deines Teams und drei API-Endpunkte in jeden Turn schiebt. Zwanzig Minuten lang läuft das hervorragend. Dann entscheidet Hermes, dass die Konversation zu lang ist, und kompaktiert sie – jede frühe Nachricht wird in einen einzigen Zusammenfassungsabsatz gefaltet, deine Konventionen inklusive. Von diesem Moment an fragt das Modell nach Dingen, die du längst beantwortet hast, und dein Plugin ahnt nicht, dass überhaupt etwas passiert ist.

Es gibt eine leisere Variante desselben Lochs: Du hältst pro Session eine kleine Indexdatei auf der Platte. Eine Kollegin löscht vierzig Sessions aus der Desktop-App, und dein Index bleibt exakt so fett wie vorher – voll mit Einträgen, die niemand mehr nachschlagen wird.

Beide Probleme haben dieselbe Wurzel: Plugins konnten sehen, dass „ein Tool aufgerufen wurde“ und dass „ein Turn begonnen hat“, aber nicht, dass „das Transkript neu geschrieben wurde“ oder dass „diese Session nicht mehr existiert“. Zwei Patches, die am 18. Oktober gemerged wurden, schließen die Lücke – ein brandneuer post_compaction-Hook und ein on_session_delete-Hook, der jeden Einstiegspunkt zum Löschen abdeckt.

Warum Plugins das vorher nicht sehen konnten

Das Plugin-System hatte bereits einen Lifecycle-Satz: on_session_start, on_session_end, on_session_finalize, on_session_reset. Die markieren Zeitpunkte, keine Umschreibungen von Inhalten.

Kompaktierung ist eine interne Aktion. Sie kann mitten im Turn passieren, wenn der Kontext vollläuft, direkt nachdem ein riesiges Tool-Ergebnis eingetroffen ist, oder wenn der Agent proaktiv veraltete Tool-Ausgaben kürzt – oder wenn ein Mensch /compress tippt. Für ein Plugin war all das unsichtbar. Das Löschen von Sessions ist dieselbe Geschichte von der anderen Seite: hermes sessions delete, Einzel- und Sammellöschungen aus dem Desktop, /exit --delete, der Sweep leerer Sessions, Retention-Pruning, Ghost-Row-Wartung. Sie alle enden in denselben Datenbankmethoden, aber Plugins bekamen keinen Callback.

Also taten Plugin-Autoren, was Menschen ohne Signal tun: den Session-Store pollen, aus Token-Zahlen raten, ob eine Kompaktierung stattgefunden hat, Verzeichnisse nach Waisen durchsuchen. Im besten Fall langsam, im schlechtesten falsch.

post_compaction: zurückholen, was die Zusammenfassung gefressen hat

Die Registrierung läuft wie bei jedem anderen Hook:

def register(ctx):
    ctx.register_hook("post_compaction", on_compaction)


def on_compaction(session_id, kind, reason, **kwargs):
    # flag it, then re-inject from the next pre_llm_call
    mark_session_for_reinjection(session_id, kwargs.get("messages_removed"))

Was der Callback bekommt:

Feld Bedeutung
session_id Die Session, die das kompaktierte Transkript jetzt hält
old_session_id Die Session, in der sie startete, wenn die Kompaktierung rotiert hat; leerer String bei Kompaktierung an Ort und Stelle
kind summarize (volle Kompaktierung), micro_summarize, prune_tool_results, provider_native (Codex app-server)
reason Was sie ausgelöst hat: manual (/compress), pre_api, post_tool, overflow, gateway_hygiene, between_turns, proactive_prune, provider, unknown
before_tokens / after_tokens Grobe Token-Schätzungen, None wenn unbekannt
messages_removed Wie viele Nachrichten weg sind, None wenn unbekannt

Wichtig ist: Der Hook feuert einmal für jede Kompaktierung, die tatsächlich greift, und zwar auf jedem Pfad – CLI, TUI- und Desktop-Backends, Gateway-Agenten, Cron-Sessions, das /compress des Gateways, native Codex-Kompaktierung, Mikro-Kompaktierung und proaktives Pruning von Tool-Ergebnissen. Versuche, die nichts verändert haben – eine abgebrochene Zusammenfassung, eine durch einen Cooldown blockierte, eine abgelehnte, weil sie das Transkript verlängert hätte, ein leeres /compress – feuern nicht. Du musst also nie nachträglich überlegen, ob eine „Kompaktierung“ ein No-Op war.

Ein Muster, das wirklich funktioniert: Merk dir, dass diese Session eine Auffrischung braucht, und speise sie beim nächsten pre_llm_call neu ein. Der Hook selbst ist ein Beobachter – sein Rückgabewert wird ignoriert, und du kannst die Konversation nicht von innen heraus umschreiben. Das ist Absicht: Die Kompaktierung ist bereits festgeschrieben, und sie mittendrin zu ändern würde Konsistenz brechen.

_pending = {}

def on_compaction(session_id, kind, reason, messages_removed=None, **kw):
    if kind in ("summarize", "micro_summarize"):
        _pending[session_id] = {"kind": kind, "removed": messages_removed}

def inject(session_id, conversation_history, **kw):
    if session_id in _pending:
        _pending.pop(session_id)
        return "Project rules: every new endpoint needs OpenAPI annotations; migrations go through alembic only."
    return None

def register(ctx):
    ctx.register_hook("post_compaction", on_compaction)
    ctx.register_hook("pre_llm_call", inject)

on_session_delete: die eigene Buchhaltung ausgleichen

Der zweite Hook übernimmt das Aufräumen. Er feuert genau einmal, nachdem das Löschen oder das beabsichtigte Archivieren committet ist:

Parameter Bedeutung
session_id Die Session, auf die sich die Operation bezog
reason delete, archive, empty_sweep, prune, ghost_prune, profile_repair
profile Das Profil, dem der Store gehört (default für das Root-Home)
deleted_ids Jede entfernte Zeile (Cascade-Kinder, Verknüpfungen der Compression-Chain, Sammelzeilen), wenn mehr als eine betroffen war
sessions_dir Das sessions/-Verzeichnis des Profils, wenn der Aufrufer eines hatte
def on_delete(session_id, reason, profile=None, deleted_ids=None, sessions_dir=None, **kw):
    ids = deleted_ids or [session_id]
    for sid in ids:
        drop_sidecar(sid)          # your per-session cache / index
    log.info("cleaned %d session rows (%s)", len(ids), reason)

def register(ctx):
    ctx.register_hook("on_session_delete", on_delete)

Zwei Details in diesem Patch übersieht man leicht, und sie sind es wert, gekannt zu werden.

Erstens: Desktop hat vorher nie gefeuert. Einzelne Lösch- und Archivieraktionen aus Desktop und Dashboard laufen nicht über SessionDB.delete_session, sondern über einen Runtime-Session-Receipt-Pfad, weshalb die ältere Verdrahtung dort stumm blieb. Dieser Pfad feuert jetzt ebenfalls, und ein exakter Retry spielt das Receipt erneut ab, ohne ein zweites Event auszulösen.

Zweitens: Archivieren zählt mit. Archivieren heißt, der Nutzer hat die Session versteckt; das Transkript bleibt und kann wieder hervorgeholt werden. Für ein Plugin bedeutet das meistens „dieser Thread ist vorbei“ – wenn du eine Aufgabe verfolgt hast, ist das Archivieren durch den Nutzer ein guter Moment, sie zu schließen. Nur ein echter Wechsel von nicht-archiviert zu archiviert feuert, deshalb bleiben automatische Sweeps für inaktive Sessions und Profil-Adoption ruhig.

Ein drittes Ding, im selben Zeitfenster behoben: Slash-Commands kennen ihre Session

Eine verwandte Änderung verdient eine Erwähnung. Ein Plugin-Slash-Command, das mit ctx.register_command registriert wurde, bekam früher nur den rohen Argument-String – keine Ahnung, wer es ausgeführt hat, auf welcher Plattform, in welcher Session. Jetzt bekommt ein Handler, der diese Keywords deklariert, sie mitgeliefert:

def _handle_whoami(raw_args, *, session_id="", platform="", source=None):
    user = source.user_id if source is not None else "local user"
    return f"{user} on {platform}, session {session_id or '(none yet)'}"

def register(ctx):
    ctx.register_command("whoami", handler=_handle_whoami, description="Show the calling session")

Verfügbar sind die Keywords session_id, session_key, platform und source (das vom Adapter abgeleitete Identitätsobjekt mit user_id, chat_id, thread_id und Verwandten) – oder du deklarierst einfach **kwargs und nimmst alle. Bestehende Plugins brauchen null Änderungen: Ein Handler mit einem Argument wird weiterhin als handler(raw_args) aufgerufen, und async-Handler werden weiterhin awaited.

Ein Sicherheitshinweis: source sind Identitätsinformationen, keine Autorisierung. Ein sensibles Plugin-Command sollte anhand stabiler IDs (user_id, chat_id) entscheiden und im Zweifel ablehnen, wenn source gleich None ist.

Drei Grenzen, um die du herum designen solltest

  • Beide sind Beobachter. Rückgabewerte werden ignoriert; ein Callback, der eine Exception wirft, wird geloggt und übersprungen. Das Löschen oder die Kompaktierung ist bereits festgeschrieben – ein Plugin kann weder ein Veto einlegen noch einen Rollback erwarten.
  • Callbacks sind zeitlich begrenzt durch plugins.hook_callback_timeout (standardmäßig 30 Sekunden). Wenn du in einem Hook Netzwerkaufrufe machst, setz dein eigenes Timeout; was hängt, ist dein Callback, nicht die Konversation.
  • Die Zustellung ist prozesslokal, ohne Exactly-once-Garantie. Das Delete-Event feuert nur in dem Prozess, der das Löschen committet hat. Externe Seiteneffekte aus dem Callback sollten einen verpassten oder doppelten Aufruf tolerieren.

Ab wann du es nutzen kannst

Beide Hooks wurden am 18. Oktober in den Main-Branch gemerged, nach dem letzten Release v0.22.2 vom 15. Oktober – sie leben also nur auf main und sind in keiner veröffentlichten Version enthalten. Wenn du Hermes von PyPI oder aus einem Update-Channel installiert hast, kannst du sie heute nicht nutzen; der Main-Branch aus dem Quellcode ist der Weg, sie früh auszuprobieren.

Für die vollständige Hook-Liste und die exakten Auslösebedingungen ist die offizielle Event-Hooks-Seite maßgeblich; die Plugins-Command-Referenz der Seite deckt die CLI-Oberfläche ab. Wenn du in diesem Bereich baust, passen zwei frühere Artikel gut dazu: der Plugin-Token-Broker zeigt, wie ein Plugin einen Nutzer in eine Drittanbieter-API einloggt, ohne je den Token zu halten, und der Compaction-Prompt-Rebuild erklärt, wie die Prompts während der Kompaktierung neu zusammengesetzt werden. Du schreibst ein Memory-Plugin? Memory-Konsolidierung im Dreaming-Stil nimmt den anderen Weg – aufräumen, während es ruhig ist, statt auf Kompaktierung zu reagieren.

Kurz gefasst

post_compaction beantwortet „was mache ich, wenn mein eingespeister Kontext weggesummarisiert wird“. on_session_delete beantwortet „wer räumt meinen Kram auf, wenn die Session weg ist“. Keiner der beiden ändert, was Hermes tut; sie legen lediglich zwei Momente offen, die vorher intern waren. Für Plugin-Autoren ist das eine ganze Klasse von Polling, Raten und defensiven Fallbacks, die du jetzt löschen kannst.