Ein umbenanntes Hermes-Profil hinterließ ein Gespenst


Du hast das Profil deines Telegram-Bots von mybot in assistant umbenannt. hermes profile rename mybot assistant lief sauber durch, das Verzeichnis wanderte von ~/.hermes/profiles/mybot nach ~/.hermes/profiles/assistant, der Shell-Alias kam mit – und der Bot antwortete im Gruppenchat weiter. Ein paar Tage später scrollst du in einer alten Unterhaltung nach oben und siehst, dass sie immer noch unter mybot einsortiert ist, einem Profil, das es nicht mehr gibt, während die Routing-Tabelle des Gateway weiterhin Schlüssel wie agent:mybot:* führt. Die andere Spielart desselben Bugs: Du löschst ein Profil, und ein paar Tage später ist sein Verzeichnis wieder da, mit einer Cache-Datei darin. PR #112653 behebt beides (#111926 und #112592).

Drei Begriffe vorweg. Ein Profil ist ein eigenständiger Hermes-Agent – eigene Config, eigene Credentials, eigenes Memory, eigene Sessions – der unter ~/.hermes/profiles/<Name> lebt. Ein Routing Key ist die Zeichenkette, mit der das Gateway entscheidet, zu welchem Profil und welcher Session eine eingehende Nachricht gehört; seine Form ist agent:<Profil>:<Session>. Ein multiplexendes Gateway ist ein einziger Gateway-Prozess, der mehrere Profile gleichzeitig bedient und den Routing-Index im Speicher hält.

Das alte Rename verschob nur das Verzeichnis, nicht die Identität

Der alte Code erledigte die Dateisystem-Hälfte: Profilverzeichnis umbenennen, Shell-Alias umbenennen. Die Datenbank-Hälfte wurde nie angefasst. Das tut weh, weil sehr viele Zeilen im State Store von Hermes den Profilnamen selbst als Schlüssel verwenden. Nach einem Rename stand dort weiterhin mybot, und mybot existierte auf der Platte nicht mehr – die Zeilen zeigten auf ein Gespenst.

Nichts explodiert sofort, es sickert. Alte Sessions passen nicht mehr zusammen, eine Heartbeat-Zeile meldet weiter ein Profil, das weg ist, Delivery Obligations (die Einträge für „diese Nachricht ist noch nicht zugestellt und braucht einen Retry“) hängen unter dem toten Namen, und die Zuordnung der Telegram-Topics stimmt nicht mehr. Wer mehrere Profile hinter einem Gateway betreibt, trifft das zuerst; die Grenzen dieses Aufbaus beschreibt unser Leitfaden zu Multi-Instance-Profilen.

Warum die Identitätstabellen den Profilnamen als Schlüssel nutzen

Für die Datenbank ist der Profilname die Identität. Ändert sich der Name, wechselt der Agent im State Store seine Identität – die Migration ist also kein Dateiumzug, sondern ein Rename plus ein Umschreiben jeder Zeile, die über diesen Namen verschlüsselt ist. PR #112653 listet genau auf, welche Zeilen dazugehören:

  • sessions.session_key, sessions.profile_name, sessions.origin_json
  • gateway_heartbeats.profile
  • delivery_obligations
  • die Telegram-Topic-Tabellen
  • der gateway_routing-Index (sowohl der Schlüssel als auch das eingebettete JSON)

Es gibt eine zweite Grenze, die man kennen sollte – was dem Profil folgt und was der Session folgt – und der Leitfaden zu Session-Grenzen geht sie durch.

Was jetzt migriert wird: eine Transaktion plus ein In-Memory-Index

Die Datenbankseite ist SessionDB.rekey_profile_state(old, new) in hermes_state_gateway.py: eine einzige Transaktion, die jede über den Profilnamen verschlüsselte Zeile auf den neuen Namen umschreibt. Vorher prüft sie auf Kollisionen – hat der neue Name schon Zeilen, die krachen würden, verweigert sie, statt ein Durcheinander zu schreiben. Sie ist idempotent und überspringt Tabellen, die noch nicht angelegt wurden.

Ein reines Datenbank-Rewrite reicht aber nicht. Ein laufender Multiplexer schreibt seine _entries aus dem Speicher regelmäßig zurück in die Datenbank und würde das Rewrite damit überschreiben. Genau dafür gibt es SessionStore.rekey_profile_routing(old, new) in gateway/session.py: Es schlüsselt den Routing-Index im Speicher um – die Einträge und origin.profile – und persistiert das Ergebnis. Deshalb gehört die Migration einem lebenden Gateway: Der Index liegt in seinem Prozess, also hat es das letzte Wort.

Die Kette läuft so. rename_profile ruft _migrate_profile_identity in hermes_cli/profile_identity.py auf. Ist ein Multiplexer aktiv, delegiert sie an das Control-Socket-Verb migrate-profile-identity; hält kein Gateway den Store, macht die CLI das dauerhafte Rewrite selbst. In jedem Fall ist die Migration nie fatal für das Rename – im schlimmsten Fall bekommst du eine Warnung, ein umbenanntes Profil und Identitätszeilen unter dem alten Namen.

Ein Profil sicher umbenennen, Schritt für Schritt

hermes profile list              # ansehen, was du gleich umbenennst
hermes profile rename mybot assistant
hermes profile show assistant    # neuen Namen und Anzeigeinfos prüfen
hermes profile list              # in der Liste steht assistant, nicht mybot

Lies die Ausgabe des Renames. Warnt sie, dass das lebende Gateway die Session-Identität nicht migrieren konnte, ignoriere das nicht – behebe es mit dem Befehl aus dem nächsten Abschnitt. Wenn du nicht weißt, ob dein Gateway ein Multiplexer ist, folge einfach der Ausgabe: keine Warnung heißt, die Migration ist durch; mit Warnung startest du das Gateway neu und versuchst es erneut. Alle Flags dieser Befehle stehen in der hermes profile-Befehlsreferenz, und die Installationsseite erklärt, wie Hermes überhaupt auf die Maschine kommt.

Wenn die Migration nicht lief: Retry mit migrate-identity

Das Rename lässt sich nicht wiederholen – profiles/mybot ist weg. Dafür gibt es einen eigenen Befehl:

hermes profile migrate-identity <old-name> <new-name>

Eine echte Sitzung sieht so aus:

hermes profile rename mybot assistant
# ⚠ Profile was renamed, but the live gateway could not migrate session identity (…).
#   Restart the gateway, then run:
#     hermes profile migrate-identity mybot assistant

hermes profile migrate-identity mybot assistant
# ✓ Session/routing identity migrated: mybot → assistant

Der erste Block sagt dir: Das Profil ist umbenannt, aber das lebende Gateway konnte die Session-Identität nicht migrieren. Er nennt zwei Auswege – das Gateway neu starten (es lädt den Routing-Index beim Start aus der Datenbank, damit landet die Migration) oder es stoppen, denn ohne Prozess über dem Store macht der Befehl das dauerhafte Rewrite selbst.

Eigenschaften, die man kennen sollte:

  • Die Migration richtet sich nach den Zeilen, die noch den alten Namen tragen; profiles/mybot muss also nicht existieren. Geprüft wird nur der neue Name.
  • Sie ist idempotent: Ein erneuter Lauf einer abgeschlossenen Migration gelingt, es bleibt nichts umzuschlüsseln.
  • Der Exit-Code ist ungleich null, wenn ein lebendes Gateway die Migration verweigert oder wenn eine Datenbank das Rewrite ablehnt – eine Routing-Kollision, ein Lock oder eine der beiden Datenbanken, die scheitert, während die andere erfolgreich ist. Die Fehlermeldung nennt Datenbank und Problem.

Das Default-Profil umzubenennen setzt nur einen Anzeigenamen

Vielleicht möchtest du einen schöneren Namen für das Default-Profil. Hier lauert die Falle: Die interne ID des Default-Profils ist immer default, denn ~/.hermes ist das Installations-Root – es lässt sich nicht wirklich umbenennen. Was der Befehl stattdessen setzt, ist ein Anzeigename:

hermes profile rename default Harumesu   # Unicode kein Problem: 小助手

Dieser Anzeigename erscheint in hermes profile list und show, im /profile-Chat-Befehl, im Dashboard und in der Desktop-App inklusive der Bot-Mode-Roster. Er ist reine Darstellung: -p default, Service-Namen, Cron-Jobs und jede andere Referenz nutzen weiterhin die kanonische ID default. Gespeichert wird er als display_name in ~/.hermes/profile.yaml – lösche die Zeile, um es zurückzudrehen. Auch benannte Profile können einen display_name tragen (er überlebt ein echtes Rename), aber bei ihnen benennt rename das Profil selbst um.

Ein gelöschtes Profil bekommt eine Grabplatte – nichts darf es wiederbeleben

Löschen sieht so aus:

hermes profile delete coder

Der Befehl stoppt das Gateway, entfernt den systemd-/launchd-Dienst, entfernt den Befehls-Alias und löscht alle Daten des Profils. Zur Bestätigung musst du den Profilnamen eintippen; --yes überspringt das. Das Default-Profil lässt sich nicht löschen (es ist ~/.hermes selbst) – für einen kompletten Abriss nimm hermes uninstall.

Was danach früher schiefging: Ein Thread, der den toten Home-Pfad noch mit sich trug, konnte einen späten atomaren Write machen, und das mkdir(parents=True) davor baute den Verzeichnisbaum wieder auf. Dein gelöschtes coder-Profil tauchte wieder auf der Platte auf, mit nichts als einer Cache-Datei darin.

#112592 schließt diesen Weg. utils._atomic_write und atomic_roundtrip_yaml_update / atomic_roundtrip_yaml_save legen ihre Elternverzeichnisse jetzt über mkdir_under_hermes_home an – den Tombstone-Guard –, sodass jeder atomare JSON-, Text-, Bytes- oder YAML-Write ein mit Grabplatte versehenes benanntes Home mit FileNotFoundError verweigert und nichts hinterlässt. Die verstreuten mkdir(parents=True)-Aufrufe in den Callern laufen durch denselben Guard: Modell-Cache, models.dev-ETag, Lifecycle-Sentinel, Secret-Cache, MCP-OAuth, Memory-Store und der mechanische Sweep der übrigen Pre-Write-Stellen; zwei Stellen, die ein explizites mode= brauchen, behalten ihr mkdir hinter assert_named_profile_home_live.

Der Guard greift nicht pauschal. Hat der Zielpfad keinen nachweisbaren profiles/<Name>-Vorfahren, ist er ein No-op: das Default-Home, versteckte .staging-Verzeichnisse und fremde Pfade, die nur zufällig profiles enthalten, schreiben weiter normal.

Fallstricke und Kurzfassung

  • Lies die Rename-Ausgabe. Eine gescheiterte Migration lässt das Rename selbst nie scheitern, sie warnt nur – starte das Gateway neu und führe hermes profile migrate-identity <alt> <neu> aus.
  • Der Retry ist idempotent, ein Lauf im Zweifel kostet also nichts.
  • Ein lebendes Multiplexer-Gateway hat Vorrang, weil es den Routing-Index im Speicher hält; stoppst du es stattdessen, macht der Befehl das dauerhafte Rewrite selbst.
  • Das Default-Profil wird nie wirklich umbenannt – du setzt nur einen Anzeigenamen, die interne ID bleibt default.
  • Nach dem Löschen bleibt gelöscht. Der Tombstone-Guard verhindert, dass ein später Write aus einem gelöschten benannten Profil wieder ein Home-Verzeichnis macht.