hermes update travou, mentiu sobre estar atualizado ou recusou sua CA — quatro correções mescladas


É sexta-feira à noite e você digita hermes update antes de desligar o computador. A tela para em Fetching... e não anda por dez minutos. Você aperta Ctrl+C, tenta em outra máquina, e dessa vez a resposta é imediata: Already up to date! — só que, depois de reiniciar, hermes --version continua mostrando a release antiga. A pior variante é a terceira: ela até move o código, não reporta nada de errado, e você assume que está em dia.

Essas três experiências vêm de quatro bugs separados, todos mesclados em main em 2026-09-28. Nenhum deles afeta suas conversas do dia a dia, mas cada um quebra exatamente a operação que você procura quando mais precisa dela. Veja o que os dispara, qual era a causa raiz e o que o updater faz agora.

1. Clones shallow: o update trava no fetch até dar timeout

O instalador faz o clone com --depth 1 para economizar tempo — só o histórico do commit mais recente. Isso vai bem até o checkout ficar atrasado o suficiente.

Em um clone com profundidade 1, um git fetch origin main comum faz o servidor enviar toda a ancestralidade de cada branch lateral mesclada além da fronteira shallow. Esse download não tem como terminar dentro do limite de rede de 300 segundos. E o passo de unshallow que resolveria isso estava agendado depois de um update bem-sucedido — então nunca rodava: timeout, update falho, sem unshallow, o mesmo timeout na próxima vez.

Agora a ordem se inverte: antes do fetch limitado, heal_shallow_history preenche o grafo de commits (só commits, com orçamento de 900 segundos, e trazendo a branch de destino junto), de modo que o fetch com limite que vem em seguida seja de fato pequeno.

O mesmo lote trata de duas falhas mais sutis:

  • Crash de index-pack --promisor no git 2.53+. Em certos objetos locais, o pack-objects bate em um BUG() interno. O estrago é que o estado fica persistido no repositório, então todo fetch posterior — a checagem de update e o próprio update — morre nele. A correção é deliberadamente estreita: apenas nessa assinatura exata de crash, tenta uma vez mais com -c remote.origin.promisor=, por invocação, sem tocar na config do repositório.
  • --filter=tree:0 transformava clones completos em parciais silenciosamente. O updater passava esse filtro em todo fetch de tag, e git fetch --filter grava remote.origin.promisor e partialclonefilter como efeito colateral — então um clone completo e saudável virava um clone parcial, e todo fetch seguinte carregava uma dependência de promisor. Os fetches de tag agora reutilizam o filtro do próprio clone: um clone parcial repete seu filtro, um clone completo busca sem filtro.

Dois comandos dizem se você está nesse tipo de máquina:

git rev-parse --is-shallow-repository      # true significa shallow
git config --get remote.origin.partialclonefilter

2. Ele disse “Already up to date!” enquanto o código acabara de mudar

Essa é mais cruel: você acha que nada aconteceu, quando na verdade o pull aconteceu e a sincronização de dependências, a migração de config e o restart do gateway foram todos pulados.

O gatilho é um checkout detached — por exemplo, porque você fez rollback antes com git checkout <tag>. Nesse estado, o updater movia o HEAD para main e só então contava HEAD..origin/main. Sua main local já apontava para origin/main, então a contagem era zero: ele imprimia Already up to date! e saía, mesmo que o código que você estava rodando de fato tivesse acabado de ser substituído. A contagem agora parte do commit que estava realmente em execução.

O caminho de rollback estava errado do mesmo jeito. Se o update terminasse com erro de sintaxe, ele rodava git reset --hard na branch de update: um usuário em detached acabava parado em main, e os commits de uma branch de feature estacionada eram escritos em main. Agora isso se divide em dois casos:

  • checkout detached: git checkout --detach <sha>, de volta ao commit em que o update começou;
  • branch estacionada: git checkout <branch>, sem mexer em main. Se outro worktree segura essa branch, o commit dela é restaurado em detached.

Uma regra cobre os dois: um update falho nunca deixa sua instalação em cima de código quebrado.

3. Atrás de uma rede corporativa, o SSL_CERT_FILE nunca chega ao git

Se sua CA raiz é distribuída pela variável de ambiente SSL_CERT_FILE, você ganha uma personalidade dividida: a checagem de versão (em Python) passa, e o git fetch falha no TLS toda vez. O motivo é entediante — o libcurl do git não lê SSL_CERT_FILE. Ele só respeita GIT_SSL_CAINFO ou um http.sslCAInfo configurado.

A correção mapeia SSL_CERT_FILE para GIT_SSL_CAINFO uma vez por update ou checagem, no ambiente do processo em vez de nos argumentos de um único comando — assim os fetches preguiçosos de promisor que um clone parcial faz também recebem a CA. Se você define GIT_SSL_CAINFO explicitamente, ou tem http.sslCAInfo na sua config do git, a sua configuração prevalece.

O mesmo lote faz o hermes pm install funcionar atrás de um mirror npm interno, que antes falhava quando não conseguia alcançar a URL pública fixada no lock file.

Suas duas checagens:

echo "$SSL_CERT_FILE"
git config --get http.sslCAInfo

4. Falhas registradas como sucessos

As três primeiras são casos de “o trabalho não foi feito”. Esta é “não foi feito e te disseram que foi”, que é o que de fato estraga sua noite.

Uma escrita recusada no .env imprimia ✓ Set mesmo assim. Quando hermes config set salva credenciais de provider, ele passa pelo caminho de escrita do .env, e esse caminho, ao detectar conflito de lock, antes imprimia uma linha e retornava. Quem chamou nunca verificava o resultado: seguia adiante, apagava os valores espelhados do config.yaml, imprimia ✓ Set e saía com 0 — e você acreditava que a credencial tinha sido gravada. Agora os quatro writers multi-store chamam require_env_writable antes de qualquer efeito colateral: a CLI sai com 1, PUT/DELETE /api/env responde 400 com o motivo, e o model.disconnect da TUI verifica todas as variáveis antes de começar.

Um dashboard que falhou ao voltar era registrado como restarted. Durante um update, o Hermes assume o controle e reinicia um dashboard que você tinha iniciado na mão. Ele reexecutava o argv de /proc do processo parado ao pé da letra — incluindo um caminho de launcher que a própria tomada de controle já tinha reescrito como um shim de shell — então o processo filho morria na hora com SyntaxError. O Popen em si funcionava, então o updater imprimia ✓ restarted de qualquer jeito. O argv agora é reconstruído no launcher vinculado à instalação que todos os outros spawns do Hermes usam, um filho que sai em menos de um segundo conta como restart falho, e um dashboard que não volta transforma o recibo do update inteiro em partial com código de saída 1.

Olhe antes de pular: dois comandos somente leitura

hermes update --check    # existe versão mais nova? (não muda nada)
hermes update --plan     # plano somente leitura: tipo de install, todo processo Hermes entre profiles, como cada um vai reiniciar

--plan é o companheiro natural dessas correções: ele imprime o que o update vai tocar e não muda nada, então é seguro rodar em um host multi-profile ativo. As outras flags que vale conhecer são --no-gateway-restart (troca o código sem reiniciar o gateway), --keep-stash e --backup / --no-backup.

O que você precisa fazer

Nada. Os quatro patches estão em main; seu próximo hermes update os pega. Se sua máquina é um clone shallow ou está atrás de uma CA corporativa, resista à vontade de dar Ctrl+C desta vez — dê tempo para ele preencher o histórico. Depois, hermes doctor é uma boa forma de confirmar que dependências, versão de config e processos em execução estão todos alinhados.

Se o fluxo de upgrade em si ainda estiver nebuloso, comece pelo nosso passo a passo de upgrade, e por como manter as skills e a config do seu profile durante um update. Vale saber: é justamente este update que finalmente elimina a camada de compatibilidade de plugins — se seu plugin ainda importa um caminho antigo, você vai descobrir logo em seguida. Vai montar tudo do zero? O guia de instalação cobre todas as plataformas.