Ein fehlgeschlagener TTS-Aufruf löschte eine Nutzerdatei: Innenansicht von Hermes' P0-Output-Path-Fix

Du bittest Hermes, eine Meeting-Zusammenfassung in Audio umzuwandeln, und du ergänzt eine Zeile im Prompt: speichere sie als meeting-notes.mp3. Dieser Dateiname ist zufällig die Aufnahme eines Kundengesprächs von heute Morgen. Ein paar Sekunden später kommt das Tool mit einem Fehler zurück — der TTS-Provider war nicht erreichbar. Du öffnest die Datei. Sie ist nicht beschädigt. Sie ist nicht leer. Sie ist weg.
Das war ein echtes P0 in Hermes: ein fehlgeschlagener Text-to-Speech-Aufruf löschte die bestehende Datei, auf die output_path zeigte. Es wurde am 9. Oktober behoben (PR #135531 — fünf Zeilen Code plus eine Testdatei). Hier ist der Mechanismus, der Fix und was er für die Nutzung des Tools bedeutet.
Warum ein Fehler deine Datei mitnehmen konnte
Fangen wir damit an, wie sich das Tool text_to_speech selbst aufräumt.
Es unterstützt Langform-Synthese, indem es den Text in Chunks aufteilt, deshalb führt jeder Aufruf ein Set namens generated_artifacts: Jeder Pfad, den dieser Aufruf berührt hat, wird dort festgehalten. Wenn der Aufruf vorbei ist — Erfolg oder Fehler —, fegt ein finally-Block das Set durch und _remove_quietly()t alles, was nicht tatsächlich ausgeliefert wurde. Dieses Design ist völlig vernünftig. Wenn Chunk 3 von 5 fehlschlägt, sollten die halbfertigen .chunk001.mp3-Dateien nicht auf der Platte zurückbleiben.
Das Problem lag im Erfassungsschritt. Vor dem Fix sah der Code so aus:
chunk_path = base_path # der Pfad, auf den output_path zeigte
if len(chunks) > 1:
chunk_path = base_path.with_name(f"{base_path.stem}.chunk{index:03d}{base_path.suffix}")
generated_artifacts.add(str(chunk_path)) # ← die eigene Datei des Nutzers wurde als "meine Temp-Ausgabe" registriert
output_path ist ein Tool-Argument, das das Modell wählen kann. Wenn es auf eine bereits existierende Datei zeigte, registrierte der alte Code diese Datei als etwas, das dieser Aufruf erzeugt hatte. Von da an:
- die Synthese gelingt → der Auslieferungsschritt
os.replace()t das neue Audio über das Ziel (das erwartete Überschreiben); - die Synthese schlägt fehl → der
finally-Sweep beansprucht den Pfad und entfernt ihn.
Der Fehlerzweig war der Ort, an dem das P0 lebte: Die Datei des Nutzers nahm an der Synthese nie teil, aber die Cleanup-Logik zählte sie als übrig gebliebene Ausgabe. Der Bug kam während eines Katalog-Reviews des Plugins wyoming-voice-bridge ans Licht.
Der Fix: daneben schreiben, erst bei Erfolg einziehen
Das neue _synthesize_chunks prüft vor dem Schreiben, ob eine Datei existiert:
if os.path.lexists(chunk_path):
# Eine Datei, die wir nicht erzeugt haben: daneben synthetisieren, damit ein fehlgeschlagener
# Versuch sie weder abschneiden noch wegfegen kann; bei Erfolg ersetzt der Auslieferungsschritt das Audio per os.replace() (Überschreiben).
chunk_path = chunk_path.with_name(f".{chunk_path.stem}.{uuid.uuid4().hex}{chunk_path.suffix}")
Wenn das Ziel bereits existiert, geht die Synthese in ein verstecktes Geschwister — .meeting-notes.9f3c….mp3. Beide Ausgänge sind jetzt sauber:
- Fehler: Der Sweep entfernt nur das Geschwister. Deine Datei wird nie geöffnet, abgeschnitten oder gelöscht.
- Erfolg: Der Auslieferungsschritt macht weiterhin ein
os.replace()des Audios auf das Ziel, der Überschreibungsvertrag bleibt also unverändert.
Neue Pfade — einschließlich des standardmäßigen, mit Zeitstempel versehenen Cache-Pfads — nehmen exakt den Code-Pfad, den sie vorher genommen haben, und die Teilausgabe eines fehlgeschlagenen Versuchs wird weiterhin aufgeräumt.
Wissenswert: Das Tool hatte bereits eine Reihe von Pfad-Gates, aber sie bewachen wohin du schreiben darfst, nicht was du überschreiben darfst:
- eine
..-Traversal-Komponente wird abgelehnt (absolute Pfade sind in Ordnung; relative Pfade dürfen nicht aufsteigen); - Steuerzeichen oder Zeilenseparatoren im Pfad werden abgelehnt;
- eine
MEDIA:-Direktive innerhalb des Pfads wird abgelehnt, damit ein gefälschtes Auslieferungs-Tag nicht weiter stromabwärts wandern kann; - geschützte Credential-/System-Positionen (
file_safetysis_write_denied/is_write_approval_required) werden abgelehnt.
Lässt du output_path weg, schreibt das Tool nach <audio cache>/tts_<timestamp>.<ext> — ein Name mit Zeitstempel, der kaum mit etwas kollidiert, das dir gehört. Das ist auch der Grund, warum die meisten Leute diesen Bug nie getroffen haben: Um ihn zu erwischen, muss man das Tool auf eine Datei zeigen lassen, die bereits existiert.
Wenn es dir schon passiert ist
Klipp und klar: Eine gelöschte Datei landet nicht im Papierkorb, und ohne Backup ist sie weg. Aber ein paar Gewohnheiten verkleinern den Radius:
- Checkpoints. Das Checkpoint-System von Hermes kann Dateiänderungen innerhalb einer Session zurückrollen; die
hermes checkpoints-Befehle lassen dich sie inspizieren und wiederherstellen. Es ist für vom Agenten gemachte Edits gebaut, hilft also nur, wenn die berührte Datei in einem getrackten Workspace liegt. - Session-Exporte. Der synthetisierte Text existiert normalerweise noch im Transkript, das Audio lässt sich also regenerieren. Eine Aufnahme kann das nicht — genau diese Unterscheidung ist das ganze Risiko.
- Halte unersetzliche Dateien aus tool-facing Verzeichnissen heraus. Eine ähnliche Klasse versehentlichen Dateiverlusts haben wir in dem Dirty-Worktree-Datenverlust-Vorfall beschrieben.
- Die vollständige Checkpoint-Oberfläche findest du in der hermes-checkpoints-Referenz.
Wie du output_path sicher verwendest
Eine Regel deckt es ab: Zeige output_path niemals auf eine Datei, deren Verlust du dir nicht leisten kannst.
- Willst du eine gesprochene Version eines Textes? Gib
output_pathgar nicht an — nimm den Standard-Cache-Pfad. - Archivierst du Audio? Verwende einen frischen Dateinamen (
summaries/2026-10-10-daily.mp3), keinen bestehenden. - Eine feste Ausgabedatei bewusst zu überschreiben ist jetzt per Vertrag sicher (Erfolg überschreibt, Fehler bewahrt), aber halte nicht die einzige Kopie von irgendetwas daneben.
Wann du es bekommen kannst
Der Fix wurde am 2026-10-09 gemergt, nachdem v0.21.6 am 8. Oktober ausgeliefert wurde, also ist er nur auf origin/main und noch in keinem getaggten Release. hermes update holt ihn von main, oder du wartest auf das nächste Release. Release-Status und die breitere Stabilitätsarbeit sind auf unserer v0.21.6-Release-Seite zusammengefasst.
Der Regressionstest kam mit ihm (tests/tools/test_tts_failure_preserves_existing.py) und pinnt den Vertrag in drei Assertions fest: Eine bestehende Datei überlebt einen Provider-Fehler, ohne dass ein Teil zurückbleibt; ein Fehler auf einem neuen Pfad lässt nichts zurück; und ein Erfolg überschreibt weiterhin. Die Lektion lässt sich auf jeden übertragen, der Tools schreibt: Cleanup-Code muss sich sicher sein, was ihm gehört. Die Datei eines anderen als eigene Temp-Ausgabe zu registrieren kostet den Nutzer seine Aufnahme.