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_idsque 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.