Hermes 'esqueceu' 798 mensagens após uma falha — o bug da amnésia de sessão, explicado e corrigido


Segunda-feira de manhã. Você abre o Telegram para retomar a conversa que deixou com o Hermes ontem à noite — e ele começa a puxar assunto sobre um tópico que já deveria estar encerrado há três dias. “Aquele PR foi mergeado?”

Não é o modelo confuso, e não é coisa da sua imaginação. Foi exatamente o que aconteceu com Teknium, o líder por trás do Hermes Agent, no início de agosto de 2026: depois de uma falha e um restart do gateway, o DM dele no Telegram silenciosamente retomou um contexto de 2,7 dias atrás, e as 798 mensagens reais trocadas nesse intervalo aparentemente desapareceram. Nada foi perdido — o Hermes simplesmente não conseguia encontrá-las.

O que aconteceu: como uma única mensagem mandou uma sessão de volta no tempo

A issue oficial de acompanhamento (#82616) inclui evidências do banco de dados de produção. O incidente se desenrolou em quatro etapas:

Quando O que aconteceu
6 ago, 16:18 Uma mensagem recebida dispara um caminho de recuperação que cria silenciosamente uma nova linha de sessão sem identidade
6 ago → 8 ago Todas as 798 mensagens reais caem nessa linha órfã; o mapeamento em memória mantém a conversa com aparência perfeitamente normal — o fork é invisível
8 ago, 17:03 O gateway falha e reinicia; o mapeamento em memória “chat → session” é descartado como obsoleto
9 ago, 09:40 Chega a próxima mensagem: o Hermes resolve o chat pela session key, e a única linha que carrega a chave é a sessão obsoleta de 3 de agosto — então ele retoma um contexto de 3 dias atrás e conversa sobre um PR mergeado dias antes

A evidência no banco mostra duas linhas: a “original” de 3 de agosto (session key completa, mas ended_at vazio e sem mensagens novas) e a órfã de 6 de agosto (session_key, chat_id todos NULL — mas com todas as 798 mensagens penduradas nela).

Em palavras simples: o Hermes “forkou” silenciosamente uma cópia sem nome da sessão, todas as mensagens foram para a cópia, um restart apagou a agenda de endereços em memória e, quando o Hermes procurou a sessão de novo, só reconheceu a linha com etiqueta de nome — então ele voltou no tempo.

Causa raiz: três falhas empilhadas, todas silenciosas

A análise aprofundada (análise completa no nível do código nos comentários da issue) convergiu para uma única cadeia:

  1. As escritas no banco da rotação de sessão falharam, e as falhas foram engolidas. O auto-reset por inatividade do Hermes encerra a sessão antiga e cria uma nova — as duas escritas falharam: uma foi registrada apenas no nível debug, e a outra foi um simples print no console. Ninguém percebeu.
  2. Mas a tabela de roteamento já havia trocado para a nova sessão. A sessão antiga virou uma “zombie” (nunca marcada como encerrada, ainda segurando a session key), enquanto a nova sessão nem existia no disco.
  3. Um escritor preguiçoso materializou a linha fantasma. Uma escrita em segundo plano posterior (ex.: contabilidade de uso de tokens) descobriu que a linha alvo não existia e a criou na hora — mas com todos os campos de identidade (session_key, chat_id, …) vazios. A órfã nasceu, e nenhum caminho de código jamais re-carimba identidade nela.
  4. Depois da falha, a resolução no restart não conseguia enxergar a órfã. Ela busca sessões pela chave e pelas informações do chat — a órfã não corresponde a nenhuma das duas, então o único candidato é a “original” zombie. Ela vence, e o contexto antigo volta.

Tem uma reviravolta fascinante: os logs não paravam de dizer possible FTS write corruption e disk=0 (leitura de 0 linhas do disco), então todo mundo primeiro desconfiou de corrupção no banco. A investigação provou que o caminho de leitura nunca toca no índice de texto completo — o problema real era o roteamento por ID de sessão: o escritor seguiu um mapa de re-roteamento, o leitor consultou o ID antigo e obteve 0 linhas. O banco esteve saudável o tempo todo, e é exatamente por isso que nenhuma mensagem sequer foi perdida.

Nada foi perdido: as 798 mensagens vivem em uma linha “escondida”

A parte mais tranquilizadora de todo o incidente: todas as 798 mensagens estão intactas — elas só estão numa linha sem etiqueta de identidade, invisível para o resolvedor. Para os usuários afetados, a issue documenta um procedimento manual de recuperação (abaixo).

A correção: prevenção + tratamento, mergeada na main em 2026-08-09

A correção chega em dois PRs — um para que isso nunca mais aconteça, outro para o estrago que já existe:

#82633 — endurecimento no lado da escrita (prevenção)

  • Identidade escrita atomicamente: session key, ID do chat e origem agora fazem parte do próprio INSERT de criação da linha (ON CONFLICT faz o backfill via COALESCE), em vez de um UPDATE best-effort após a criação. Identidade e linha agora vivem ou morrem juntas.
  • Todo refresh é uma oportunidade de reparo: o gateway atualiza as informações da sessão a cada turno; se faltar uma linha, agora ele insere a identidade completa em vez de simplesmente não fazer nada.
  • Resolução ciente de recência: após uma falha/restart, as sessões candidatas são ranqueadas por last_activity_at, com linhas que contêm mensagens primeiro, e um ID novo nunca é gerado enquanto existir uma linha com chave.
  • Erros deixam de ser silenciosos: falhas de escrita de encerramento/criação são registradas em WARNING com a consequência de roteamento explicada; exceções de leitura de transcrição não retornam mais silenciosamente uma lista vazia.
  • Também corrige a #12857: resets de sessão não perdem mais o ID da sessão pai (linhagem).

A validação foi brutal: 11 testes de regressão rodados contra a main sem correção produziram 6 falhas; depois da correção, todos passam. Uma reprodução ponta a ponta do incidente (zombie de 3 dias + órfã de 798 mensagens em um banco real) resolve de volta para a conversa ativa.

#82712 — hermes sessions repair-routing (tratamento)

Um novo comando que “resgata” sessões órfãs, projetado em torno de um princípio fail-closed:

hermes sessions repair-routing              # dry-run primeiro: relata o que encontrou, não muda nada
hermes sessions repair-routing --apply      # repara de fato, após confirmação
  • Detecção: varre as linhas de sessão do gateway sem chave mas com mensagens reais (linhas de branch/delegate/tool não têm chave por design e são excluídas, então não há falsos positivos).
  • Baseado em evidências: ele só age quando a evidência é inequívoca — ou linhagem registrada (parent_session_id apontando para uma linha com chave da mesma fonte), ou exatamente um predecessor com chave que ficou em silêncio dentro de 15 minutos do início da órfã. Dois predecessores candidatos, ou duas órfãs reivindicando um mesmo predecessor, são recusados com justificativa — adotar por engano emendaria a conversa de uma pessoa no chat de outra, então ele se recusa a adivinhar.
  • O reparo: carimba a órfã com a identidade do predecessor via COALESCE (nunca sobrescreve um valor existente), registra a linhagem e aposenta o predecessor com end_reason='superseded_by_repair' — de propósito, não o motivo normal de reset, para que o próximo restart não possa derivar de volta.

Você foi afetado?

Confira o seu perfil antes de entrar em pânico:

  • Apenas sessões de gateway: Telegram, Discord, Slack, WhatsApp e outras sessões de plataformas de mensagens. Usuários exclusivamente de CLI são estruturalmente imunes — o CLI não tem nenhuma dessas engrenagens de roteamento de sessão.
  • Maior risco: sessões de longa duração, quem reinicia/atualiza/derruba o gateway e conversas com uso intenso de multimodal.
  • Não é um caso isolado: a mesma instalação mostra 5 incidentes de orphan session desde junho (42, 34, 5, 798 e 2 mensagens) — este foi apenas o maior, e aconteceu com o desenvolvedor líder.
  • Sintoma típico: depois de um restart do gateway, o Hermes fala de assuntos antigos, não lembra dos dias recentes e menciona coisas já encerradas há muito tempo.

Atualizando e recuperando seus dados

Importante: a correção está na main (mergeada em 2026-08-09), mas ainda não está em nenhuma release — a release mais recente continua sendo a v0.20.0 (2026-08-03). É recomendado que usuários de longa data atualizem por um de dois caminhos:

  1. Esperar pela próxima release, ou
  2. Rodar a partir da main para cobertura imediata (veja o guia de instalação e a referência do comando de atualização).

Já foi afetado e quer sua conversa de volta:

  • Preferido: atualize para um build que contenha a correção e então rode hermes sessions repair-routing (dry-run primeiro, depois --apply).
  • Procedimento manual (da issue oficial, quando a ferramenta não estiver disponível):
    1. Pare o gateway;
    2. Faça backup primeiro: cp state.db state.db.bak;
    3. Copie a session_key, o chat_id, o chat_type, o origin_json etc. da linha “original” obsoleta para a linha órfã (preencha apenas os campos vazios, nunca sobrescreva);
    4. Defina ended_at e end_reason='superseded_by_repair' na linha obsoleta;
    5. Reinicie o gateway e deixe a resolução encontrar a sessão real.

Faça backup do state.db antes de qualquer etapa manual e garanta que o gateway esteja totalmente parado. Na dúvida, espere pela release com a correção e use o repair-routing — é mais seguro do que editar manualmente.

O que este incidente nos ensina

A parte mais assustadora não é o bug em si — é o silêncio. O fork da sessão aconteceu quieto, em segundo plano; a continuidade em memória sustentou a ilusão de que estava tudo bem, até um restart expor tudo. Duas lições para qualquer pessoa que rode um assistente de IA de longa duração: primeiro, verifique a continuidade da sessão após restarts e atualizações; segundo, dados do Hermes raramente desaparecem — uma conversa “perdida” geralmente é um problema de roteamento, não um problema de dados.

Para se aprofundar em gerenciamento de sessão, veja a referência do comando hermes sessions; para a release principal mais recente, confira as notas da release v0.20.0.