O /rollback reportou sucesso — e engoliu silenciosamente o trabalho não commitado no seu repositório aninhado


Parecia o seu desfazer mais seguro de todos. Um refactor saiu dos trilhos, então você rodou /rollback list, escolheu um checkpoint de duas horas atrás e digitou o número. O terminal disse sucesso. Depois você entra num subdiretório para continuar e o trabalho não commitado no repositório que está ali simplesmente sumiu — não foi para a lixeira, nem para um stash.

Isso é a issue #127073. Checkpoints fazem snapshot do seu diretório de trabalho, mas quando esse diretório contém outro repositório git, o repositório interno é registrado como um gitlink — uma referência de commit crua — e seus arquivos nunca entram no snapshot. O rollback ainda reportava sucesso, então você perdeu o trabalho e recebeu um sinal verde falso. O fix (PR #127839, recuperando #127077, de @dskwe) chegou à main em 2026-09-29.

O que os checkpoints armazenam, e o que nunca armazenaram

A mecânica é o que decide quando você se dá mal. Antes de uma chamada de write_file, patch ou terminal, o Hermes commita um snapshot do diretório de trabalho num repositório bare compartilhado:

~/.hermes/checkpoints/
  ├── store/                 # one shared bare repo
  │   ├── refs/hermes/<hash> # per-project branch tip
  │   ├── indexes/<hash>     # per-project git index
  │   └── projects/<hash>.json

Usar o próprio modelo de objetos do git mantém os snapshots baratos e diffáveis — e é exatamente daí que vem a lacuna: o git não armazena o conteúdo de um sub-repositório dentro do seu pai. Tudo o que a árvore pai guarda é um gitlink apontando para um commit. Edições que você não commitou no repositório interno não existem do ponto de vista do repositório externo.

Isso produz a pior combinação possível:

  • arquivos que o agente alterou na árvore externa são restaurados a partir do checkpoint ✅
  • a mesma operação reseta o repositório interno para o commit nomeado pelo gitlink, descartando o conteúdo não commitado ❌
  • e o comando retorna sucesso ✅ ← essa é a parte realmente perigosa

Depois do fix: recusar de antemão em vez de pedir desculpas depois

A nova regra é direta: decida antes de tocar em qualquer coisa e, se o rollback alcançaria um gitlink, não mude absolutamente nada.

O comportamento, ponto por ponto:

  • Um rollback completo (/rollback <N>) é recusado, porque restaurar tudo necessariamente cobre o gitlink.
  • Qualquer seleção que toque um gitlink também é recusada — o próprio gitlink, um arquivo abaixo dele, ou um diretório pai que o contenha. A recusa acontece antes de qualquer arquivo, index, ref ou snapshot de segurança pré-rollback ser modificado, e o erro nomeia os repositórios que não foram capturados.
  • Seleções que evitam gitlinks ainda restauram, incluindo pathspecs do Git como *.txt, :(glob)…, :(exclude)tool. A implementação resolve a seleção contra um index descartável primeiro: um wildcard que alcançaria um gitlink é recusado, e um que não alcançaria se comporta exatamente como antes. Isso importa — a tentativa anterior desse fix rejeitava todo pathspec de saída, o que removia a recuperação de *.txt que continua funcionando.
  • A guarda usa o caminho registrado no checkpoint, não o filesystem de hoje. Antes, um notes.txt capturado que depois virou um symlink para dentro de um repo aninhado não podia ser restaurado; agora pode. Na direção contrária, um gitlink registrado que desde então foi substituído por um symlink não relacionado continua sendo recusado. A validação de confinamento de caminho (_validate_file_path) não mudou.

Mais que uma recusa: os checkpoints agora avisam em voz alta

Recusar não ajuda com o que já aconteceu, então o patch também adiciona divulgação no momento da captura. Todo checkpoint que armazenou um gitlink recebe uma anotação anexada à própria mensagem:

before write_file: app.py [nested git repos not captured: tool]

Mais de três caminhos e você recebe os três primeiros mais um +N, por exemplo [nested git repos not captured: a, b, c (+2)]. Como as listagens de /rollback já renderizam a descrição do checkpoint, você vê o aviso enquanto escolhe o checkpoint — não depois de um rollback que falhou.

O mesmo patch corrigiu um bug que escondia a anotação: a superfície rollback.list da TUI e do Desktop lia uma key message enquanto o manager a chama de reason, então essas listagens nunca mostravam descrições de checkpoint. Agora está mapeado, o que torna visíveis tanto as descrições antigas quanto o novo rótulo.

Um detalhe multiplataforma: os componentes de caminho agora são separados com o Path da própria plataforma, então packages/nested e packages\nested são ambos recusados corretamente no Windows — o achado de normalização de caminho levantado na review do PR anterior.

O que fazer com isso

Uma vez que a mecânica está clara, o conselho prático é curto, e cada item economiza horas:

  1. Commite dentro do sub-repo antes de deixar um agente tocar no repositório externo. Um gitlink aponta para um commit; só conteúdo commitado é referenciado. Se você ainda não quer o trabalho na linha principal, git stash ou um commit descartável é melhor do que torcer para o checkpoint cobrir isso.
  2. Leia a lista antes de fazer rollback. /rollback list agora mostra [nested git repos not captured: …]. Ver essa anotação significa que este checkpoint só consegue restaurar arquivos externos — cuide do repositório interno você mesmo.
  3. Use pathspecs para restaurações cirúrgicas. /rollback 1 src/broken_file.py é bem mais seguro que um rollback de diretório inteiro, e wildcards voltaram a ser suportados desde que não toquem num gitlink.
  4. Não conte com isso em backends de container. Com docker, singularity, modal, daytona ou um plugin de container, os paths pertencem ao sandbox, não ao host: o Hermes não faz checkpoints para eles, e o /rollback lista os checkpoints existentes do host enquanto recusa diff e restore, tanto na CLI quanto em chats de mensagem. /diff session dá a mesma razão. Backends locais e SSH não são afetados.

Quatro guardrails que já existiam vale a pena lembrar quando uma montanha-russa vira “por que não consigo restaurar nada”: diretórios com mais de 50.000 arquivos são ignorados, um único arquivo acima de max_file_size_mb (padrão 10 MB) fica fora do snapshot, o store descarta o commit mais antigo por projeto em round-robin quando ultrapassa max_total_size_mb (padrão 500 MB), e os checkpoints são desativados de forma transparente quando o git não está no PATH.

Para ver quanto espaço o histórico ocupa, ou limpá-lo:

hermes checkpoints status     # tamanho total, contagem de projetos, detalhamento por projeto
hermes checkpoints prune      # remove checkpoints órfãos/obsoletos e faz GC do store
hermes checkpoints clear      # apaga toda a base (todo o histórico de /rollback)

Uma condição prévia: os checkpoints são opt-in. Desde a v2 eles vêm desligados por padrão — ative-os para uma sessão com hermes chat --checkpoints, ou globalmente com checkpoints.enabled: true em ~/.hermes/config.yaml. Uma vez ligados, cada projeto mantém até max_snapshots: 20 checkpoints por padrão, e a manutenção automática roda no máximo uma vez a cada 24 horas, descartando projetos sem uso por 7 dias. Se você nunca os ativou, o /rollback vai estar simplesmente vazio — isso não é um bug.

Mantenha o modelo mental claro: um checkpoint é um desfazer do que o agente acabou de fazer, não um backup de todos os arquivos que existem na sua árvore. Topamos com a mesma classe de surpresa no post sobre corrupção silenciosa de arquivos: confiar que uma ferramenta vai reverter por você só funciona quando você sabe exatamente o que ela armazenou.

A versão curta

  • Um repositório git aninhado aparece num checkpoint como um gitlink; suas alterações não commitadas nunca estiveram no snapshot.

  • Antes do fix, o /rollback sobrescrevia o repo interno e reportava sucesso — a pior combinação.

  • Agora um rollback completo, e qualquer seleção que toque um gitlink (ele mesmo, um arquivo abaixo dele ou seu diretório pai), é recusada antes que qualquer coisa mude, e o erro nomeia os repositórios não capturados.

  • Seleções que evitam gitlinks ainda restauram, pathspecs do Git incluídos, e o tratamento de symlinks e de caminhos do Windows segue o caminho registrado.

  • Checkpoints que perderam um repo aninhado são rotulados [nested git repos not captured: …] no momento da captura, visível em /rollback list, na TUI e no Desktop.

  • Na prática: commite ou faça stash dentro do sub-repo primeiro, prefira /rollback <N> <path>, e lembre que backends de container não têm checkpoints no host.

  • Os checkpoints são opt-in desde a v2 (hermes chat --checkpoints ou checkpoints.enabled: true) — uma lista /rollback vazia normalmente só significa que o store nunca foi ativado.

Referência completa de flags em hermes checkpoints; para o trabalho de integridade vizinho, veja esta rodada de correções de compactação.