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*.txtque continua funcionando. - A guarda usa o caminho registrado no checkpoint, não o filesystem de hoje. Antes, um
notes.txtcapturado 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:
- 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 stashou um commit descartável é melhor do que torcer para o checkpoint cobrir isso. - Leia a lista antes de fazer rollback.
/rollback listagora 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. - 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. - Não conte com isso em backends de container. Com
docker,singularity,modal,daytonaou um plugin de container, os paths pertencem ao sandbox, não ao host: o Hermes não faz checkpoints para eles, e o/rollbacklista os checkpoints existentes do host enquanto recusa diff e restore, tanto na CLI quanto em chats de mensagem./diff sessiondá 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
/rollbacksobrescrevia 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 --checkpointsoucheckpoints.enabled: true) — uma lista/rollbackvazia 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.