O hermes update parava os gateways e nunca os acordava: o .git que o próprio git rejeita


Numa máquina Windows, o hermes update terminou e o Telegram e o WhatsApp pararam de responder. Três linhas no log: o seu gateway chamado hermes-2 foi “pausado”, depois o fatal: not a git repository do git, depois uma promessa tranquilizadora — “Paused gateway(s) left stopped: the checkout HEAD is unreadable”, com a garantia de que o próximo comando hermes traria o gateway de volta.

Você rodou um. Nada voltou. Você rodou hermes update de novo: as mesmas três linhas, palavra por palavra.

O PR #142609, mesclado em 21 de outubro de 2026, corrige esse loop. Vale a leitura porque o bug não é “um recurso parou de funcionar” — é tratamento de erro vedando o caminho de recuperação. Essa classe de bug fica invisível até morder, e quando morde você assume que quebrou alguma coisa.

Causa raiz: um .git que o próprio git rejeita

A premissa primeiro: esse .git não está ausente, ele está presente e ilegível pelo git. Dois formatos típicos:

  • .git/HEAD está vazio, ou o arquivo HEAD sumiu por completo.
  • .git é um arquivo (não um diretório) contendo gitdir: <some path>, e esse caminho não existe mais — um resto comum de worktree ou de migração.

O updater do Hermes só faz fetch quando .git existe, então ele faz fetch alegremente contra esse .git. O git responde not a git repository e o update falha. Até aqui, nada demais.

O problema real é o registro de pausa. Antes de atualizar, o Hermes pausa os gateways em execução (para que nada esteja lendo a árvore enquanto o código muda) e registra o estado da árvore. Esse registro tem uma checagem chamada tree_is_whole, e sempre que o HEAD está ilegível ela mantinha o conjunto pausado indefinidamente. O que significava:

  1. Primeiro update: pausa o gateway, o fetch falha, HEAD ilegível, conjunto pausado mantido, gateway continua fora.
  2. Segundo update: mesmo HEAD ilegível, conjunto mantido de novo, gateway ainda fora.
  3. Para sempre.

Uma precondição quebrada (HEAD ilegível) era tratada como motivo para não recuperar — e ela nunca se consertaria sozinha.

Correção um: o registro de pausa solta

A regra nova: se toda pausa do conjunto já encontrou o HEAD ilegível no momento em que foi registrada, e nenhum passo vinculou uma movimentação de árvore depois disso, o conjunto é retomado e os gateways voltam.

O raciocínio é simples. Os gateways estavam servindo exatamente aquela árvore, e nada escreveu nela — sem estado pela metade para proteger, não há motivo para continuar segurando.

Dois casos ainda mantêm o conjunto, e isso importa se você assumir que agora ele solta incondicionalmente:

  • uma movimentação de árvore vinculada está registrada (_checkout_move registra targets e caminhos antes de o git escrever qualquer coisa);
  • o HEAD era legível na pausa e não é mais — alguma outra coisa danificou a árvore no meio do caminho, e um humano deve olhar.

Correção dois: o erro nomeia o dano e o reparo

A segunda mudança está no diagnóstico do fetch. Antes, toda causa produzia Failed to fetch updates from origin., deixando você no chute.

Agora, quando o próprio git imprime fatal: not a git repository e o checkout falha no rev-parse --show-toplevel, o Hermes imprime:

This Hermes checkout's git data is damaged ...

seguido de um comando do instalador apontado para este checkout — com o caminho:

curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --dir <path>

No lado do PowerShell é -InstallDir <path>. Isso importa para instalações em diretório customizado: elas são reparadas no lugar, em vez de empurradas de volta para o local padrão.

Por que a checagem extra de rev-parse? Porque um repositório remoto com um .git pendurado imprime a mesma linha fatal: not a git repository. Casar só pelo texto diagnosticaria errado uma quebra local, então o veredito depende de o checkout atual poder ou não ser interpretado. O hermes update --check reporta o mesmo diagnóstico.

Correção três: o instalador lida com um arquivo .git pendurado

Os dois instaladores já sabiam mover um checkout cujo HEAD está ilegível para o lado como .broken-<stamp> e clonar de novo. Mas o install.sh só fazia isso para um diretório .git; diante de um arquivo .git pendurado, ele recusava com “not a Hermes git checkout”.

Agora ele testa com -e, igual ao Test-Path do install.ps1. Os dois avisos também dizem “damaged .git” agora, em vez de apenas “interrupted clone” — os dois costumavam parecer idênticos.

Por que o fallback por ZIP ainda recusa

O updater tem um fallback por ZIP para quando o git é inutilizável: baixar um arquivo e sobrescrever. Neste cenário ele ainda recusa.

O motivo está na documentação: sem git status, ele não consegue distinguir suas edições locais de dano, e sobrescrever apagaria o trabalho de alguém. Essa regra de fail-closed (#87304) continua valendo — melhor um reparo manual do que uma sobrescrita no chute.

Se você está preso nisso agora

Se você está encarando um gateway parado, siga esta ordem (tudo comportamento de comandos já publicados):

# 1. veja o que o gateway acha que está fazendo
hermes gateway list
hermes gateway status

# 2. se ele só está pausado e nada mais o segura, traga de volta
hermes gateway restart

# 3. se o .git está mesmo danificado, repare no lugar com o instalador
#    (troque <path> pelo caminho do seu checkout)
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- --dir <path>

Quando você estiver num build que carrega esta correção, o passo 3 deixa de ser problema seu — o hermes update imprime o comando para você. Isso também aposenta um mal-entendido relacionado: a promessa de que “o próximo comando hermes vai reiniciar os gateways” nem sempre era verdade, porque ela dependia de o registro de pausa se liberar, e o código antigo nunca fazia isso.

Status de release e raio de alcance

O PR foi mesclado no fim de 21 de outubro, depois da publicação da v0.22.4 daquele dia (18:49 UTC), então não está na v0.22.4 nem em nenhuma tag — só na main por enquanto.

Os relatos de campo são de Windows (.git/HEAD apagado com a árvore de trabalho intacta), mas a causa raiz é neutra de plataforma: qualquer coisa que torne o HEAD ilegível — limpar o .git na mão, um clone interrompido, uma migração que copiou só o worktree — dispara o mesmo caminho no Linux e no macOS. O Windows apenas produz esse estado mais facilmente, por causa de locks de arquivo e esquisitices de permissão.

A lição é pequena e geral: quando a lógica de recuperação depende de uma precondição se curar sozinha, e essa precondição é justamente o que está quebrado, o sistema trava em silêncio. Agora ele ou se recupera, ou te diz exatamente onde reparar.