Vier Abstürze unter Windows, die nur Nutzer mit lokalisierter Installation treffen

Ihre Maschine läuft mit deutschem Windows. Sie installieren Hermes, starten es, und jede neue Unterhaltung killt sich selbst mit einer Zeile, die Sie nicht interpretieren können: UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9'. Stellen Sie die Systemsprache auf Englisch, funktioniert alles — Sie nehmen also an, Sie hatten Pech. Jemand anderes ist auf chinesischem Windows und sieht das Log seines Desktops mit UnicodeDecodeError: 'gbk' codec can't decode volllaufen; eine dritte Person bemerkt, dass die Datei weder getaggt noch in der Vorschau angezeigt werden kann, wenn der Agent C:\...\report.html schreibt.
Diese vier Probleme sind im Code nicht verwandt. Gemeinsam haben sie das, was sie so lange überleben ließ: Sie treten nur auf einem lokalisierten, nicht-englischen Windows auf, weshalb sie aus einer englischen Umgebung fast nie reproduziert oder gemeldet werden. Alle vier wurden in v0.21.5 gefixt (Tag v2026.9.24, veröffentlicht am 24. September 2026). Hier steht, was tatsächlich los war.
1. Ein Zonenname brachte den Aufbau des System-Prompts zum Absturz
Der Fehler sah so aus:
agent/system_prompt.py -> _zone_bits() -> now.strftime("%Z")
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce9' ...: surrogates not allowed
Ursache: Windows liefert Zonennamen in der ANSI-Codepage kodiert zurück, nicht als Unicode. Auf französischem Windows kommt Paris, Madrid (heure d'été) als cp1252-Bytes an. Ist LC_CTYPE des Prozesses UTF-8 (Pythons UTF-8-Modus oder eine Komponente wie Piper/espeak, die die prozessweite Locale aus einem anderen Thread umstellt), werden diese Bytes mit surrogateescape dekodiert, tzname() enthält also am Ende einsame Surrogate.
Der eigentliche Ärger kommt danach: datetime.strftime fügt tzname() als UTF-8 in den Formatstring ein, %Z wirft also innerhalb von strftime — bevor überhaupt ein String existiert, was bedeutet, dass der vorhandene _sanitize_surrogates-Durchlauf nie die Chance bekommt, etwas zu bereinigen.
Und weil der System-Prompt bei jedem Gesprächsstart und jeder Kompaktierung neu gebaut wird, ist das kein einmaliger Ausrutscher: Er verweigert jede neue Unterhaltung, die Sie öffnen.
Der Fix (PR #120748): ein kleiner Helfer, hermes_time.safe_strftime(value, fmt), und jede strftime-Stelle, die Locale-Text rendert, ruft ihn jetzt auf. Er verhält sich in zwei Modi:
- Auf einer gesunden Locale ist die Ausgabe byte-identisch zu
value.strftime(fmt). Das ist eine harte Anforderung, keine Nettigkeit — der System-Prompt muss stabil bleiben, sonst bricht das Prompt-Caching. Tests vergleichen die beiden direkt für naive Datetimes, IANA-Zonen und gültige Nicht-ASCII-Zonennamen. - Wenn der Text Surrogate trägt und dieser Text nur aus ASCII plus surrogate-escapten Bytes besteht, werden die Bytes zurückgewonnen und über die ANSI-Codepage (
locale.getencoding()) dekodiert, washeure d'étéwieder als Text liefert. Alles nicht Dekodierbare fällt aufU+FFFDzurück, und das Ergebnis lässt sich immer sicher nach JSON serialisieren.
Die gemeldeten Locales waren fr-FR, es-AR und de-DE.
2. Dekodierung von Kindprozessen auf chinesischem Windows
Das Log sah so aus, und zwar wiederholt:
[gateway-crash] thread Thread-NNN (_readerthread) raised
UnicodeDecodeError: 'gbk' codec can't decode byte 0x85 in position 2: illegal multibyte sequence
Ursache: _readerthread ist der Reader-Thread, den subprocess.communicate() unter Windows startet. Wenn ein Capture text=True und kein encoding= übergibt, dekodiert subprocess über subprocess._text_encoding(), also locale.getencoding() — auf chinesischem Windows cp936/GBK. Die betreffenden Kindprozesse schreiben UTF-8, jedes in GBK illegale Byte wirft also einen UnicodeDecodeError in diesem Thread, den der threading.excepthook des Serve-Prozesses dann als [gateway-crash]-Zeile ausgibt.
Der konkrete Übeltäter war hermes_cli/web_routers/files.py::_fs_git_branch, das für /api/fs/default-cwd den Befehl git branch --show-current ausführt — ein Endpunkt, den Desktop ununterbrochen pollt. git schreibt UTF-8, einschließlich Branch-Namen und der eigenen lokalisierten stderr-Ausgabe (etwa der chinesischen Meldung „not a git repository“), auf einem GBK-Host warf also jeder Poll im Reader-Thread und der Endpunkt gab einen leeren Branch zurück.
Erwähnenswert ist, warum das so lange unbemerkt blieb: Die kwargs dieses Aufrufs stehen in einem Dict-Literal ("text": True), und die zeilenbasierte Regel des Projekts zum Erkennen von text=True griff dort nie.
Der Fix (PR #120749): Diese Captures werden explizit auf UTF-8 festgelegt (encoding="utf-8" mit errors="replace"). Die Spawns in tui_gateway/ (Slash-Worker, Compute-Host, cli.exec, pdftoppm) waren bereits auf UTF-8 festgelegt; dies schließt die Klasse der In-Process-Captures.
Ein verwandtes Detail: Der in v0.21.0 hinzugefügte Starter für ein Gateway mit versteckter Konsole ließ das Symptom auf der Maschine des Melders verschwinden, die Reader selbst wurden aber nie gefixt. Ein verschwindendes Symptom ist keine Ursache, und dieser Fix ist die Ursache.
3. Pfade, die mit C:\ beginnen, ließen sich weder taggen noch in der Vorschau anzeigen
Symptom: Der Agent schreibt unter Windows C:\Users\me\report.html oder einen UNC-Pfad \\server\share\..., und Desktop zeigt kein Artifact-Tag. Wenn doch ein Tag erschien, kam das Preview-Label als \Users\me\report.html heraus; der Renderer-Fallback hängte den Pfad an das Arbeitsverzeichnis und erzeugte /repo/C:\....
Ursache: looksLikePath und isPreviewableTarget in fallback-model/targets.ts akzeptierten nur file://, /, ./ und ~/, ein absoluter Windows-Pfad fiel also durch den Test „ist das ein Pfad“, toolPreviewTarget gab nichts zurück und es erschien kein Tag. Auf der Renderer-Seite behandelte localPreviewTarget alles, was nicht mit / beginnt, als relativ und hängte es an cwd. Und previewName parste C:\... mit new URL(...), wo c: als URL-Schema gelesen wird, sodass nur der Rest als Label überlebte.
Der Fix (PR #121074): Ein neues isWindowsAbsolutePath in lib/path-compare.ts akzeptiert einen Laufwerksbuchstaben mit beiden Slashes und ein UNC-Präfix \\; looksLikePath akzeptiert diese Pfade; isPreviewableTarget führt seinen HTML-Check auf allem aus, was looksLikePath akzeptiert, sodass C:\x.html in der Vorschau angezeigt wird und C:\x.txt nicht, passend zum POSIX-Verhalten; localPreviewTarget hängt absolute Windows-Pfade nicht mehr an cwd; previewName beschriftet sie mit dem Dateinamen. Eine private Kopie desselben Regex im Check für Anhang-Uploads ruft jetzt die geteilte Funktion auf (ein separater Commit, keine Verhaltensänderung).
4. Eine 10-Sekunden-Probe erklärte einen Kaltstart zum Fehler
Symptom: Bei einem Windows-Kaltstart antwortet das lokale Backend von Desktop per HTTP, dann meldet die App einen Boot-Fehler, reißt den Kindprozess ab und loggt eine Kaskade, die mit „Ignoring stale Hermes backend exit (1)“ beginnt.
Ursache: Bei einem Windows-Kaltstart stockt die Event-Loop des lokalen Backends 12–28 Sekunden lang, während der GIL beim Import von Gateway-Plattformmodulen gehalten wird. Das feste Budget der WebSocket-Probe beim Boot betrug 10 Sekunden (DEFAULT_CONNECT_TIMEOUT_MS), sie verriegelte also einen gesunden Kaltstart als Fehler, und alles Weitere folgte.
Der Fix (PR #120750): Für ein Backend, das diese App selbst gestartet hat, wartet die Probe über das Basisbudget von 10 Sekunden hinaus weiter, solange der Kindprozess am Leben ist, bis zu einer Obergrenze von 90 Sekunden — dasselbe Budget, das der Schritt zur Port-Ankündigung für einen Kaltstart bereits zugesteht. Das Fortschrittssignal ist die Prozess-Liveness, nicht die Aktualität der Ausgabe, denn der Stillstand ist stumm: Der Prozess gibt nichts aus, bis seine Loop sich erholt (der Loop-Heartbeat in web_server.py loggt event loop stalled erst danach).
Die beiden Fehlerformen bleiben unterscheidbar: Ein lebender Loopback-Kindprozess, der den TCP-Connect angenommen, aber nicht auf das Upgrade geantwortet hat, ist beschäftigt, die Probe wartet also; eine Verweigerung oder Auth-Ablehnung kommt weiterhin sofort als error/close; ein Kindprozess, der beendet wird, scheitert beim nächsten 1-Sekunden-Check; ein lebendes Backend, das nie antwortet, scheitert an der Obergrenze und zeigt das bestehende Boot-Failure-Overlay, dessen Retry das Fenster neu lädt. Remote-Gateways, die Schaltfläche „Test remote“ und das Anhängen an ein Host-Backend behalten das feste Budget von 10 Sekunden.
Release-Status dieser vier Fixes
Alle vier sind in v0.21.5 (Tag v2026.9.24, 24. September 2026), ein Upgrade auf diese Version bringt sie also mit — main muss nicht mitverfolgt werden. Das ist erwähnenswert, denn eine Reihe anderer Fixes wurde am selben Tag gemergt, nachdem der Tag geschnitten war, und die sind hier noch nicht enthalten.
Was Sie in Ihrem eigenen Install prüfen sollten
Wenn Sie einen der oben genannten Punkte getroffen haben, bestätigt dies nach dem Upgrade:
hermes --version # expect v0.21.5 (2026.9.24)
- Zonennamen-Crash: Öffnen Sie eine neue Unterhaltung auf einem lokalisierten Windows — kein
surrogates not allowedmehr, und der System-Prompt sollte stabil bleiben (Ihre Prompt-Cache-Trefferrate sollte sich nicht verschieben). - Kindprozess-Dekodierung: Öffnen Sie Desktop auf chinesischem Windows — keine fortlaufenden
[gateway-crash] ... UnicodeDecodeError-Zeilen mehr. - Windows-Pfade: Bitten Sie den Agenten, eine
C:\...\report.htmlzu schreiben — das Artifact-Tag sollte erscheinen und die Vorschau funktionieren; dasselbe gilt für einen UNC-Pfad. - Kaltstart: Starten Sie Desktop nach dem Löschen des Gateway-State kalt — kein Boot-Failure-Overlay und keine Kaskade „Ignoring stale Hermes backend exit“.
Alle vier liegen in jenem Patch-Fenster ohne Release Notes; die vollständigere Liste steht auf unserer Release-Seite zu v0.21.5 und im Selbstcheck-Leitfaden zum v0.21.5-Fenster. Die am selben Tag gemergten Streaming-Zustellungs-Fixes, die noch nicht in dieser Version sind, behandelt der Delivery Sweep.
Die Kurzfassung
Was diese vier teilen, ist zugleich das, was sie für Nutzer mit lokalisierter Installation so leidig macht: Sie lassen sich fast nie auf einem englischen System reproduzieren. Ein Zonenname, die Kodierung eines Kindprozesses, ein Pfad mit Laufwerksbuchstabe und ein Import-Stillstand beim Boot zeigen sich alle nur im Schnittpunkt von „nicht-englisches Windows“ und Windows selbst. Auf einer lokalisierten Windows-Installation sollte ein Upgrade auf v0.21.5 eine Menge unerklärlicher Fehlschläge beseitigen.