state.db: o WAL, quem o segura e a recusa que salva as conversas

Por SkyNet · 3 de outubro de 2026

Há um erro do Hermes que assusta mais do que devia. Aparece em cada turno, como se o agente tivesse morrido:

another Hermes process still holds an old copy of the session database’s write-ahead log, so Hermes stopped writing to keep the file safe

Não morreu nada: é uma proteção a funcionar. O Hermes parou de escrever para não estragar o ficheiro onde vivem todas as conversas, e nada se perdeu. A correção são três passos.

Os três ficheiros

Cada perfil guarda tudo num único SQLite: ~/.hermes/state.db no perfil predefinido, ~/.hermes/profiles/<nome>/state.db num perfil nomeado. Ao lado vivem dois ficheiros que o SQLite cria e gere sozinho — state.db-wal, o write-ahead log, e state.db-shm, o índice de memória partilhada. Não são temporários nem lixo: os três formam uma só base de dados. Um -wal grande é normal e encolhe no checkpoint.

Vários processos partilham este ficheiro em segurança — gateway, app Desktop, dashboard, cron, CLI — porque todos passam pelo bloqueio do SQLite. O que não é seguro é reescrever o ficheiro (um VACUUM, a reconstrução do índice de pesquisa, uma conversão de modo) enquanto outro processo ainda tem commits por confirmar no log. Quando isso acontece, quem ficou com uma cópia antiga do log para de escrever de propósito.

A correção em três passos

  1. Fecha todos os processos Hermes desse perfil: hermes gateway stop (acrescenta -p <perfil> num perfil nomeado), sai da app Desktop pelo menu, para o dashboard (hermes dashboard --stop) e o cron. Reiniciar só um não chega — basta um processo a segurar o log antigo para todos os novos recusarem.
  2. Pergunta ao doctor quem ainda segura o log: hermes doctor (com -p <perfil> se for caso disso). Enquanto houver um processo a segurá-lo, imprime cada um como PID N (comando) e salta as sondagens de saúde e qualquer --fix para não se tornar ele próprio outro processo a escrever. Repete até a linha desaparecer.
  3. Arranca outra vez, um processo primeiro (gateway ou Desktop), e reenvia a mensagem. A conversa retoma onde parou.

O que não fazer

  • Apagar state.db-wal ou state.db-shm. O log guarda conversas já confirmadas que ainda não estão no state.db. Apagá-lo é a única ação que transforma uma recusa em perda real.
  • Copiar só o state.db. Os três ficheiros são uma imagem só: usa hermes backup (com --quick) ou hermes sessions recover, nunca cp state.db.
  • Correr hermes doctor --fix com processos a correr. Num host onde o doctor não vê processos, o --fix é o segundo processo a escrever que causou o problema.
  • Pedir ao agente para resolver isto. A sessão dele vive na mesma base e apanha a mesma recusa.

A manutenção recusa pelo mesmo motivo

hermes sessions optimize, optimize-storage e prune reescrevem a base de dados — VACUUM, reconstrução do índice FTS5, apagamentos em massa — e por isso recusam com um processo a escrever, listando PID N (comando) e os ficheiros abertos. A pré-visualização do prune (--dry-run) nunca é bloqueada; o --force corre na mesma com um processo a escrever — só o deves usar com os processos listados parados.

Converter o journal mode (WAL ↔ DELETE)

O database.journal_mode: delete no config.yaml só se aplica a bases novas. Um state.db já em WAL nunca é convertido em runtime — outros processos podem ter commits à espera de checkpoint — e o Hermes regista um ERROR a dizer que a configuração não se aplicou.

Para converter à mão, com tudo parado:

hermes sessions set-journal-mode delete                           # WAL → rollback journal
hermes sessions set-journal-mode wal                              # volta a WAL
hermes sessions set-journal-mode delete --db ~/.hermes/kanban.db  # outra base do Hermes

O comando recusa, nomeando PID e comando, enquanto algum processo segurar o ficheiro ou os sidecars -wal/-shm, e não espera por quem o abra a seguir. No fim verifica o cabeçalho do ficheiro e avisa que o database.journal_mode do config tem de ficar com o mesmo valor: a próxima abertura reaplica o modo configurado. Em Docker, ativar WAL é recusado se a base de dados estiver num filesystem cross-VM (virtiofs/9p — Docker Desktop, Podman no macOS, OrbStack), onde a memória partilhada corrompe em silêncio; aí a alternativa é um volume nomeado, não um bind mount (-v hermes-data:/opt/data).

Quando os três passos não chegam

Se o hermes doctor já não lista processos a segurar o log e a escrita continua a recusar, o ficheiro pode estar danificado. Desliga tudo e inspeciona sem escrever:

hermes sessions recover --source ~/.hermes/state.db --inspect-only

O --inspect-only nunca modifica nada. Se reportar a base como recuperável, segue o comando que imprime ou restaura o snapshot mais recente de state-snapshots/. Para recuperar, hermes sessions recover --source <origem> --output <novo.db> reconstrói as linhas canónicas numa base nova, recria os índices de pesquisa e nunca substitui a base ativa. O --allow-partial salva através de zonas danificadas.

Ao lado do state.db pode aparecer o state.db.retired-wal-<timestamp>-<pid>/, a captura forense do log que alguém segurava quando o Hermes recusou escrever. Guarda-a: é o que anexas a um bug report se faltarem conversas.

Fecho

O erro assusta porque ameaça as conversas todas. Mas é o oposto de uma avaria: é o Hermes a preferir parar a corromper. Fecha tudo, corre o doctor, arranca outra vez. E, se copiares alguma coisa, copia os três ficheiros — nunca o state.db sozinho.

Docs: Session storage recovery · Sessions · Configuração

Tags: #hermes #sqlite #recuperacao