A sessão que você apagou voltou: dois bugs e uma nova proteção


Você apaga uma sessão que deu errado no app desktop, atualiza a lista e lá está ela de novo. Ou apaga uma pela CLI e só depois percebe que a sessão nunca mais armazena outra resposta: o agente continua respondendo, o histórico continua vazio, e o único rastro é uma linha no log — Session DB append_message failed: FOREIGN KEY constraint failed. As duas histórias são reais, e as duas saíram do mesmo lote de correções.

O PR #125157, mesclado em 2026-09-27, muda o comportamento em si: enquanto um turno ativo é dono de uma sessão, a exclusão é recusada em vez de ser silenciosamente desfeita.

Primeiro, dois bugs reais

A issue #123583 documentou o primeiro por completo: a linha da sessão desaparece do store enquanto o objeto do agente que a possui continua vivo. Sua flag interna _session_db_created ainda acredita que a linha existe, então o flush chama append_message uma vez, a chave estrangeira rejeita, e a falha só é registrada no log — sem retry, e nada observável depois, porque rollback é rollback. Isso se repete a cada turno. O chat parece completamente normal enquanto seu transcript durável para de crescer.

O segundo é a correção dele (PR #124117, resgatada do #123641): uma vez que uma linha é confirmada como ausente, o próximo flush recria a sessão sob o mesmo id e reproduz todo o prefixo do transcript, não apenas o novo turno. A primeira tentativa disso armazenava só o novo turno, porque os dicts de histórico pareciam “já salvos” — o tipo de detalhe que é sempre óbvio em retrospecto. A correção também insiste que apenas uma linha confirmada como ausente seja curada; outras falhas de escrita não disparam rebuild.

A nova proteção: recusar a exclusão, não desfazê-la

Superfície Sessão mantida por um turno ativo Ociosa / fechada por compactação
CLI sessions delete, export --delete-after-verified Recusada, código de saída 1, Cannot delete active session Excluída
Exclusão única no desktop 409 Excluída
Exclusão em massa no desktop Roots ativos ignorados e reportados em skipped_active, com um toast mostrando as duas contagens Excluída
DELETE do servidor de API 409 Excluída
session.delete do TUI 4023 (o código existente de “cannot delete an active session”) Excluída
sessions prune Linhas ativas poupadas (sem mudança) Removidas

Alguns detalhes de implementação vale a pena conhecer:

  • A proteção é verificada dentro da própria transação de escrita da exclusão (o mesmo caminho _write_guards_reject / _guarded_ids que o prune já usava), então nada pode mudar entre a checagem e a exclusão.
  • Se uma sessão root estiver ociosa mas um filho delegado ainda tiver um lease ativo, a exclusão também é recusada — a exclusão cascatearia para esse filho.
  • Sessões que terminaram (por exemplo, fechadas por compactação) sem nenhum turno dono delas continuam podendo ser excluídas.
  • A exclusão em massa faz uma checagem de guarda em lote e só recorre a checagens por root quando algo está de fato protegido.

O que você vai ver

hermes sessions list                                   # encontre o alvo
hermes sessions delete 20260928_101500_ab12cd         # ativo -> Cannot delete active session, exit 1
hermes sessions export 20260928_101500_ab12cd --delete-after-verified   # exporte e verifique, depois exclua
hermes sessions prune --older-than 30d                # linhas ativas são sempre poupadas

No TUI, uma exclusão de sessão ativa retorna 4023; no desktop ou na API, é 409. Na prática, o caminho mais simples é deixar o turno terminar, ou dar /stop nele, e excluir depois.

Então o caso do “retorno” ainda é um problema?

Está tratado, dos dois lados. A correção de rebuild (#124117) cobre “a linha sumiu mas a conversa não”: a sessão volta com todo o prefixo do histórico na próxima escrita bem-sucedida, então uma exclusão no meio do voo não custa mais os turnos seguintes. A proteção (#125157) cobre o outro lado: uma exclusão ou acontece ou explica o porquê, sem estado intermediário em que a UI mostra a sessão como excluída e o backend continua escrevendo.

Se você suspeita que já tem resíduo desse tipo de bug, olhe as estatísticas do próprio store antes de fazer algo drástico. Limites de sessão explica quando as sessões são divididas e quando terminam, o post sobre state.db cobre recuperação em nível de banco de dados, e as flags exatas estão na nossa referência de hermes sessions.

Status de lançamento

As três mudanças foram mescladas em 2026-09-27, depois da tag de release v0.21.5 (v2026.9.24), então estão apenas em main: hermes update as traz.

A conclusão

“A sessão voltou” e “a sessão sumiu mas continua recebendo turnos” parecem duas histórias diferentes, mas são uma só: a exclusão nunca se alinhou com o turno que estava usando a sessão. A correção torna isso explícito — recusar enquanto o turno está ativo e dizer ao usuário por quê; reconstruir uma linha genuinamente perdida na próxima escrita. O único hábito que vale mudar é pequeno: antes de excluir uma sessão, confirme que o turno dela realmente terminou.