Uma chamada TTS falha apagou um arquivo do usuário: por dentro da correção P0 do caminho de saída do Hermes

Você pede ao Hermes para transformar o resumo de uma reunião em áudio e acrescenta uma linha ao prompt: salve como meeting-notes.mp3. Acontece que esse nome de arquivo é justamente a gravação de uma chamada de cliente que você fez hoje de manhã. Poucos segundos depois, a ferramenta retorna um erro — o provider de TTS estava indisponível. Você abre o arquivo. Não está corrompido. Não está vazio. Ele sumiu.
Esse foi um P0 de verdade no Hermes: uma chamada de text-to-speech que falhou apagou o arquivo existente para o qual output_path apontava. Foi corrigido em 9 de outubro (PR #135531 — cinco linhas de código mais um arquivo de teste). Veja o mecanismo, a correção e o que isso significa para a forma como você usa a ferramenta.
Por que uma falha podia levar seu arquivo embora
Comece por como a ferramenta text_to_speech faz a própria limpeza.
Ela dá suporte a síntese de textos longos dividindo o texto em chunks, então cada chamada mantém um conjunto chamado generated_artifacts: todo caminho que a chamada tocou é registrado ali. Quando a chamada termina — com sucesso ou com falha — um bloco finally varre o conjunto e chama _remove_quietly() em tudo que não foi de fato entregue. Esse design é totalmente razoável. Se o chunk 3 de 5 falha, os arquivos .chunk001.mp3 pela metade não deveriam ficar no disco.
O problema estava na etapa de registro. Antes da correção, o código era assim:
chunk_path = base_path # o caminho para o qual output_path apontava
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)) # ← o arquivo do próprio usuário foi registrado como "minha saída temporária"
output_path é um argumento da ferramenta que o modelo pode escolher. Quando ele apontava para um arquivo que já existia, o código antigo registrava esse arquivo como algo que esta chamada tinha produzido. A partir daí:
- a síntese dá certo → a etapa de entrega faz
os.replace()do novo áudio sobre o alvo (a substituição esperada); - a síntese falha → a varredura do
finallyreivindica o caminho e o remove.
O ramo de falha é onde morava o P0: o arquivo do usuário nunca participou da síntese, mas a lógica de limpeza o contava como saída deixada para trás. O bug apareceu durante uma revisão de catálogo do plugin wyoming-voice-bridge.
A correção: escreva ao lado e só mude de endereço em caso de sucesso
O novo _synthesize_chunks verifica se existe um arquivo antes de escrever:
if os.path.lexists(chunk_path):
# Um arquivo que não criamos: sintetize ao lado dele, para que uma tentativa falha não possa
# truncá-lo nem removê-lo; em caso de sucesso, a etapa de entrega faz os.replace() do áudio sobre ele (substituição).
chunk_path = chunk_path.with_name(f".{chunk_path.stem}.{uuid.uuid4().hex}{chunk_path.suffix}")
Quando o alvo já existe, a síntese vai para um arquivo irmão oculto — .meeting-notes.9f3c….mp3. Agora os dois desfechos são limpos:
- Falha: a varredura remove apenas o irmão. Seu arquivo nunca é aberto, truncado ou desvinculado.
- Sucesso: a etapa de entrega continua fazendo
os.replace()do áudio sobre o alvo, então o contrato de substituição segue igual.
Caminhos novos — incluindo o caminho de cache padrão com timestamp — seguem exatamente o mesmo fluxo de código de antes, e a saída parcial de uma tentativa falha continua sendo limpa.
Vale saber: a ferramenta já tinha um conjunto de travas de caminho, mas elas guardam onde você pode escrever, não o que você pode substituir:
- um componente de travessia
..é rejeitado (caminhos absolutos são aceitos; caminhos relativos não podem subir); - caracteres de controle ou separadores de linha no caminho são rejeitados;
- uma diretiva
MEDIA:dentro do caminho é rejeitada, então uma tag de entrega forjada não consegue seguir adiante; - locais protegidos de credenciais/sistema (o
is_write_denied/is_write_approval_requireddofile_safety) são rejeitados.
Omita output_path e a ferramenta escreve em <audio cache>/tts_<timestamp>.<ext> — um nome com timestamp muito improvável de colidir com algo que seja seu. É também por isso que a maioria das pessoas nunca encontrou esse bug: para atingi-lo, é preciso apontar a ferramenta para um arquivo que já existe.
Se já aconteceu com você
Sem rodeios: um arquivo desvinculado não vai para a lixeira e, sem backup, ele se foi. Mas alguns hábitos reduzem o estrago:
- Checkpoints. O sistema de checkpoint do Hermes consegue reverter mudanças de arquivo dentro de uma sessão; os comandos
hermes checkpointspermitem inspecioná-las e restaurá-las. Ele é feito para edições do agente, então só ajuda se o arquivo tocado estiver em um workspace rastreado. - Exportações de sessão. O texto que foi sintetizado normalmente ainda existe no transcript, então o áudio pode ser regerado. Uma gravação, não — e é exatamente essa diferença que constitui todo o risco.
- Mantenha arquivos insubstituíveis fora de diretórios expostos às ferramentas. Documentamos uma classe semelhante de perda acidental de arquivos no relato do incidente de perda de dados de worktree suja.
- Para a superfície completa de checkpoint, veja a referência de hermes checkpoints.
Como usar output_path com segurança
Uma regra resolve tudo: nunca aponte output_path para um arquivo que você não pode perder.
- Quer uma versão falada de algum texto? Não passe
output_pathde jeito nenhum — fique com o caminho de cache padrão. - Está arquivando áudio? Use um nome de arquivo novo (
summaries/2026-10-10-daily.mp3), não um existente. - Substituir de propósito um arquivo de saída fixo agora é seguro por contrato (sucesso substitui, falha preserva), mas não deixe a única cópia de nada ao lado dele.
Quando você pode obtê-la
A correção foi integrada em 2026-10-09, depois de a v0.21.6 ser lançada em 8 de outubro, então ela está apenas em origin/main e ainda não em nenhuma release tagueada. O hermes update vai buscá-la do main, ou você pode esperar a próxima release. O status da release e o trabalho de estabilidade mais amplo estão resumidos na nossa página da release v0.21.6.
O teste de regressão veio junto (tests/tools/test_tts_failure_preserves_existing.py), fixando o contrato em três asserções: um arquivo existente sobrevive a uma falha do provider sem deixar nada parcial para trás, uma falha em um caminho novo não deixa nada para trás, e um sucesso ainda substitui o arquivo. A lição vale para qualquer um que escreva ferramentas: código de limpeza precisa ter certeza do que lhe pertence. Registrar o arquivo de outra pessoa como sua própria saída temporária custa ao usuário a gravação dele.