Das Hermes in Docker verlor die Hälfte seines Abhängigkeitsbaums — das Image hat nie eine Basislinie festgehalten


Du öffnest morgens das Hermes in deinem Docker-Container, und der API-Server startet nicht: aiohttp not installed. Die Messaging-Adapter sind weg, die MCP-Server sind weg, gehostete Agents erscheinen als degraded. Du hast keine Konfiguration geändert, das Image hat sich nicht bewegt — das Einzige, was passierte, ist, dass der Container am Tag zuvor eine optionale Abhängigkeit installiert hat.

Das ist kein kaputtes Image. Es ist ein leiserer Mechanismus: Die erste Abhängigkeits-Generation, die ein Container für sich selbst baut, wird zur Basislinie für jeden Boot danach. Wenn die ins Image eingebackenen Abhängigkeiten nie festgehalten wurden, warf die erste selbst gebaute Generation sie still weg.

Wie eine „Abhängigkeits-Generation“ funktioniert

Abhängigkeiten in Hermes sind keine Einbahnstraßen-Installation, die einfach liegen bleibt. Sie werden von einem Dependency Manager (PM) verwaltet, der bei jeder Installation oder jedem Update eine Generation erzeugt: Er hält eine Selection (welche Extras vorhanden sein sollen) plus einen Stempel fest, und gegen diese Generation werden spätere Boots geprüft.

Das Design ruht auf einer Annahme: Die Basislinie ist festgehalten, also können Inkremente darauf aufbauen. Wenn du später eine optionale Abhängigkeit lazy installierst, sollte sie sich auf die festgehaltene Basislinie drauflegen (union). Ist die Basislinie leer, wird diese eine Lazy-Installation zur Basislinie und verdrängt alles andere.

security.allow_lazy_installs ist standardmäßig true, das heißt, fehlende Pakete können bei Bedarf installiert werden. Genau diese Bequemlichkeit verstärkt das Problem, denn jede Lazy-Installation kann „baue eine neue Generation“ auslösen.

Warum Container der perfekte Ort sind, um gebissen zu werden

Das Docker-Image tut von sich aus etwas völlig Vernünftiges — es backt acht Extras direkt in /opt/hermes/.venv ein:

all  messaging  otlp  anthropic  bedrock  azure-identity  matrix  google-chat

Ausgepackt heißt das: das Gateway, OTLP-Telemetrie, Anthropic-/Bedrock-/Azure-SDKs und -Credentials, der Matrix-Adapter und der Google-Chat-Adapter. Alles, was du zum tatsächlichen Betrieb brauchst.

Das Problem ist, dass zur Image-Build-Zeit diese Extras nie als PM-Dependency-Selection festgehalten wurden. Die Abfolge ist also:

  1. Der Container baut seine erste Generation wegen irgendeiner Lazy-Installation — ein Plugin mit eigenen Abhängigkeiten oder du, der du einen On-Demand-Kanal wie fal, parallel-web oder exa aktivierst;
  2. Diese Generation wird nur aus dem in diesem Moment angeforderten Extra gebaut;
  3. Ab dem nächsten Boot ist diese unvollständige Selection die Basislinie.

Das ist die Anfangsszene: API-Server, Messaging-Adapter und MCP-Server alle auf einmal weg. Seit v0.21.6 hat jeder Container auf dem offiziellen Image, der eine eigene Generation gebaut hat, das vor sich.

Klar gesagt: Die Diagnose wurde nicht verpasst. Ein anderer Contributor kam unabhängig zur selben Schlussfolgerung — keine festgehaltene Basislinie im Image — und schlug einen Fix für neue Installationen vor. Was ausgeliefert wurde, tut über die Prävention hinaus zwei weitere Dinge: Es repariert bereits kaputte Volumes und hält die angeforderte Extra-Liste fest statt eines Inventars dessen, was installiert wurde.

Warum die angeforderte Liste und nicht ein Inventar? Weil sich bei einem echten Image-Build zeigte, dass das Inventar Meta-Extras wie all, cron und sms auslässt — Extras, denen kein eigenes Paket gehört und die sich deshalb nicht aufzählen lassen, aber trotzdem in der Basislinie bleiben müssen.

Fix eins: Der Image-Build hält seine Basislinie fest

Der Image-Build schreibt diese Extras jetzt in die Dependency-Fakten des Stores, über genau denselben Aufzeichnungspfad, den der gebündelte Build bereits nutzte. Jede spätere Installation ist damit ein Union auf dieser Basislinie statt eines Neuanfangs.

Fix zwei: Jeder Boot stellt wieder her, was gefehlt hat

Prävention hilft nur neuen Images. Was ist mit den schon kaputten Volumes? Sie heilen beim Boot.

Jeder Boot vergleicht jetzt die vom Image festgehaltenen Extras mit der Selection, die das Volume tatsächlich hält, und stellt alles Fehlende über den normalen expliziten Sync-Pfad wieder her. Ein Volume, das auf ["fal"] oder auf eine leere Liste zusammengeschrumpft ist, erholt sich beim ersten Boot mit einem gefixten Image, ohne manuellen Schritt.

Einige Randfälle werden sorgfältig behandelt:

  • Scheitert die Wiederherstellung — etwa offline —, bootet das venv des Images trotzdem, und die fehlenden Extras bleiben als fehlend vermerkt, sodass eine Lazy-Installation vor dem nächsten Boot sie nicht erneut entfernen kann;
  • Ein festgehaltenes Extra, das die Quelle nicht mehr deklariert, wird ignoriert (Abgleich über den PEP-685-Namen);
  • Eine fehlerhafte festgehaltene Tatsache (schlechte Extras, fehlender oder nicht-string-Stempel) meldet invalid recorded dependency state, statt still als „nichts festgehalten“ gelesen zu werden;
  • enabled-features.json wird bewusst nicht geschrieben, allow_lazy_installs verhält sich also exakt wie zuvor. Diese Änderung berührt die Basislinie und sonst nichts.

Dieselbe Fehlerform, ein zweites Mal: Die Desktop-App reißt die TUI mit

Dasselbe Batch enthält ein weiteres Beispiel für „ein Teil scheitert, alle zahlen“ — und diesmal zahlt ein Headless-Server.

Ein Quell-Update bereitet Abhängigkeiten vor, indem es ui-tui, web und apps/desktop als eine Union npm ci installiert. Nur die Desktop-App zieht native Addons, die einen Compiler brauchen: node-pty, electron. Ein Ubuntu-Server also, der die Desktop-App nie ausgeführt hat und nur g++ 9 besitzt — der -std=gnu++20 ablehnt —, ließ die gesamte Abhängigkeitsinstallation scheitern, und TUI und Web-UI wurden nie gebaut, obwohl sie nichts mit der Desktop-App zu tun haben. Jeder Start gab a source update could not be finished automatically (6 attempts) aus.

Jetzt versucht dieser Fall einmal ohne die Desktop-App erneut, baut TUI und Web-UI wie gewohnt und vermerkt den Desktop-Build als offen:

Desktop app build owed: desktop dependencies failed

Außerdem gibt er einen wirklich nützlichen Ausweg aus:

ⓘ If this host never runs the desktop app, hermes uninstall --gui removes it and
  future updates stop building it.

Auf einer Headless-Maschine ist das die sauberste Antwort — die Desktop-App gar nicht erst mitführen:

hermes uninstall --gui

Der Hinweis erscheint in beiden Situationen, die ihn brauchen: nach diesem erfolgreichen erneuten Versuch und bei jedem Headless-Linux-Update (kein DISPLAY oder WAYLAND_DISPLAY), das die Desktop-App auswählt.

Was zu prüfen ist und was zu tun ist

Läuft in Docker:

  1. Bestätige zuerst, dass das Symptom passt — API-Server, Messaging-Adapter und MCP-Server verschwinden zusammen, Agents werden als degraded gemeldet;
  2. Zieh das gefixte Image und starte neu; ein kaputtes Volume stellt seine fehlenden Extras beim ersten Boot wieder her, und das Volume muss nicht neu erstellt werden;
  3. Wenn du das Image noch nicht wechseln kannst, stellt das explizite Installieren der fehlenden Extras diesen Boot wieder her — aber die Basislinie ist weiter falsch, die nächste Lazy-Installation kann sie also erneut entfernen.

Läuft auf einem Headless-Server:

  1. Wenn Updates weiter mit could not be finished automatically scheitern, prüfe, ob die nativen Abhängigkeiten der Desktop-App der Blocker sind;
  2. Wenn dieser Host die Desktop-App nie ausführt, entferne sie mit hermes uninstall --gui aus dem Checkout;
  3. Künftige Updates bauen dann node-pty und electron nicht mehr für ein Frontend, das du nicht nutzt.

Zwei Gewohnheiten, die sich verallgemeinern lassen:

  • Abhängigkeitsprobleme sind selten „ein Paket ist kaputt“ und viel häufiger eine Generation, deren Basislinie falsch geschrieben wurde. Wenn etwas plötzlich ein ganzes Fähigkeitsset verliert, verdächtige die Basislinie, bevor du Pakete einzeln neu installierst;
  • Lazy-Installationen sind gerade deshalb bequem, weil sie „fehlend“ in „der neue Standard“ verwandeln. Willst du langfristige Stabilität, schlägt eine festgehaltene Basislinie jedes Mal eine Ad-hoc-Installation.

Das Fazit

Beide Fixes in diesem Batch drehen sich um eines: Der Abhängigkeitszustand braucht einen vertrauenswürdigen Basislinien-Datensatz. Das Docker-Image backte acht Extras ein, ohne sie festzuhalten, also löschte die erste selbst gebaute Generation des Containers sie; die nativen Module der Desktop-App ließen sich nicht bauen und rissen eine unabhängige TUI und Web-UI mit. Das erste ist gelöst durch Aufzeichnen zur Build-Zeit und Wiederherstellen beim Boot; das zweite durch Degradieren auf den Teil, der tatsächlich gebraucht wird.

Für die umgebende Betriebslektüre siehe Hermes in einem geteilten Container betreiben, das state.db-WAL-Problem und was zu tun ist, wenn ein Update deine Festplatte sprengt. Die Befehlsdetails stehen auf den Referenzseiten hermes uninstall und hermes update, und die Installationsanleitung ist der Startpunkt von Null.