9.8 KiB
Relatório técnico — Issue #69678 e PRs relacionados
Data da análise: 22 de julho de 2026
Repositório: NousResearch/hermes-agent
Issue principal: #69678 — SQLite connections leaked in delivery, async delegation, and verification evidence ledgers
PR principal: #69681 — fix(gateway,tools,agent): close leaked SQLite connections in delivery
Resumo executivo
A issue #69678 descreve um bug real: três ledgers SQLite usam a conexão como context manager, mas nunca a fecham explicitamente. Em processos de gateway de longa duração, as conexões e seus descritores de arquivo podem permanecer vivos até a coleta pelo garbage collector, acumulando descritores para o banco principal, -wal e -shm. O processo eventualmente pode atingir RLIMIT_NOFILE e começar a falhar com [Errno 24] Too many open files em componentes não relacionados.
A causa raiz apresentada está correta. O PR #69681 corrige os 21 call sites identificados e preserva as semânticas existentes de transação e locking.
Conclusão: #69678 não é duplicata exata de #69567. Ambas pertencem à mesma classe de bug, mas afetam módulos diferentes. O PR #69681 deve ser tratado como fix irmão do PR #69594, não como implementação duplicada.
Escopo afetado
| Módulo | Call sites afetados | Operações que acionam o ledger | Risco |
|---|---|---|---|
gateway/delivery_ledger.py |
5 | Registro, atualização, recuperação, pruning e inspeção de entregas | Muito alto; executado no fluxo frequente de respostas finais |
tools/async_delegation.py |
13 | Dispatch, conclusão, recuperação, claim, release e confirmação de entrega | Alto durante delegações em background |
agent/verification_evidence.py |
3 | Resultado de terminal, edição do workspace e leitura de status | Cresce com operações de desenvolvimento e verificação |
Total confirmado: 21 call sites.
Causa raiz
Os módulos usam o seguinte padrão:
with _connect() as conn:
...
O context manager de sqlite3.Connection controla a transação:
- sucesso: commit;
- exceção: rollback;
- saída do bloco: não executa
conn.close().
Consequentemente, o bloco with transmite uma falsa impressão de gerenciamento completo do recurso. A transação termina, mas o lifecycle da conexão não termina de forma determinística.
Em modo WAL, uma conexão pode manter descritores associados a:
- arquivo principal do banco;
- arquivo
-wal; - arquivo
-shm.
Em processo curto, encerramento ou coleta rápida pode mascarar o defeito. Em gateway long-lived, sob tráfego recorrente, acúmulo pode alcançar o soft limit de descritores e causar falhas em leituras de configuração, arquivos temporários, sockets e outros bancos SQLite.
Relação com #69567 e PR #69594
Issue #69567 encontrou a mesma falha em cron/executions.py. Uma execução normal de cron abre conexões em create_execution(), mark_execution_running() e finish_execution(). O relato mediu crescimento de descritores até atingir limite do processo.
PR #69594 propõe um _transaction() que:
- abre a conexão;
- preserva commit/rollback com
with conn:; - fecha a conexão em
finally; - fecha também quando inicialização de PRAGMA/schema falha.
O PR #69681 aplica o mesmo modelo a três ledgers não alterados pelo PR #69594.
Decisão sobre duplicidade
Não marcar #69678 como duplicata de #69567.
Justificativa:
- causa técnica idêntica;
- arquivos e call paths diferentes;
- #69594 altera somente ledger de cron;
- mesmo após #69594, os 21 sites de #69678 continuariam vazando conexões;
- busca de PRs relacionados não encontrou outro PR cobrindo os três módulos de #69678.
Classificação correta: issues irmãs pertencentes à mesma classe de defeito.
Avaliação do PR #69681
Estado observado
- aberto;
- não é draft;
- GitHub o considera mergeable;
- 1 commit;
- 6 arquivos alterados;
- 512 adições e 29 remoções;
- sem reviews ou comentários no momento da análise.
Solução implementada
Cada módulo recebe um context manager equivalente a:
@contextmanager
def _transaction() -> Iterator[sqlite3.Connection]:
conn = _connect()
try:
with conn:
yield conn
finally:
conn.close()
Além disso, _connect() passa a fechar a conexão caso PRAGMA ou inicialização de schema falhe depois de sqlite3.connect() ter retornado com sucesso.
Pontos corretos
- fechamento determinístico em sucesso, early return e exceção;
- commit e rollback continuam delegados ao context manager nativo;
- locking existente é preservado;
_transaction()não adquire_DB_LOCK, evitando lock nesting novo;_prune()emdelivery_ledgercontinua lock-free;- contrato schema-on-connect é mantido;
- todos os 21 call sites são migrados;
- nenhuma configuração, schema de ferramenta ou superfície core nova é adicionada.
Nenhum defeito funcional foi identificado no patch analisado.
Minimal fix recomendado
Solução do PR é pequena no comportamento de produção e resolve a causa raiz. Recomendação: manter _transaction() local em cada módulo.
Não criar agora um helper SQLite global compartilhado. Isso aumentaria escopo, acoplamento e risco para resolver três módulos independentes. Uma abstração compartilhada só deve surgir após demanda concreta e contrato comum comprovado.
Possíveis reduções sem mudar o desenho:
- encurtar docstrings repetidas dos três
_transaction(); - compartilhar fixture de tracking apenas se já existir local apropriado na suíte;
- evitar refatorações adjacentes.
O volume +512/-29 vem principalmente dos três arquivos de regressão. O fix runtime em si permanece cirúrgico.
Avaliação dos testes
O PR adiciona testes para:
- fechamento em operações normais;
- update sem linha correspondente;
- exceção durante operação SQL;
- falha durante inicialização do schema;
- igualdade entre número de conexões abertas e fechadas.
Os testes usam conexões SQLite reais envolvidas por um proxy que registra chamadas a close(). Isso valida diretamente o contrato quebrado e evita depender do timing do garbage collector.
Melhoria opcional
Adicionar teste Linux de integração contando /proc/self/fd após várias operações. Esse teste reproduziria o sintoma externo, mas pode ser específico de plataforma e mais frágil. Não deve bloquear merge se a suíte direta de lifecycle e suítes existentes estiverem verdes.
Resultados citados pelo autor não foram reexecutados nesta análise, pois checkout local contém várias alterações pré-existentes e branch do PR não foi aplicada. Antes do merge, CI deve confirmar:
tests/gateway/test_delivery_ledger_fd_leak.py
tests/tools/test_async_delegation_fd_leak.py
tests/agent/test_verification_evidence_fd_leak.py
tests/gateway/test_delivery_ledger.py
tests/gateway/test_delivery_ledger_producer.py
tests/tools/test_async_delegation.py
tests/agent/test_verification_evidence.py
Issues relacionadas
Mesma classe geral de lifecycle SQLite, mas escopos diferentes:
- #69567: cron execution ledger;
- #60859: leak de
SessionDBem early return; - #30027: listagem de boards kanban;
- #28802: helpers kanban specify;
- #36111: lifecycle de
ResponseStoreno API server; - #37369: crescimento de descritores de
response_store.db.
Essas issues demonstram padrão recorrente: uso de context manager transacional interpretado incorretamente como gerenciamento completo da conexão.
Possíveis ocorrências residuais
Busca estática encontrou padrões semelhantes fora do escopo do PR, incluindo:
gateway/readiness.py;- templates FastMCP em
optional-skills/mcp/fastmcp/templates/database_server.py.
Esses locais não devem ser incluídos automaticamente em #69681. Cada ocorrência precisa de:
- confirmação de que conexão não possui outro owner;
- reprodução ou teste de lifecycle;
- análise da frequência e duração do processo;
- fix separado quando comportamento estiver comprovado.
Expandir #69681 para uma auditoria global contrariaria objetivo de mudança cirúrgica.
Solução estrutural futura
Após merge dos fixes urgentes, abrir tarefa separada de auditoria dirigida:
- localizar
with sqlite3.connect(...),with connect(...)ewith _connect(...); - classificar conexões por lifecycle: per-operation ou long-lived;
- exigir
contextlib.closing,try/finally close()ou helper transacional para conexões per-operation; - adicionar teste de regressão somente para call paths reais;
- documentar em guia interno que
sqlite3.Connectioncontext manager não fecha a conexão.
Evitar mudança mecânica global: alguns componentes podem manter conexão deliberadamente durante lifetime do serviço.
Recomendação final
- Não fechar #69678 como duplicata.
- Tratar #69594 e #69681 como fixes irmãos.
- Aprovar desenho do PR #69681 após CI verde.
- Não ampliar PR para ocorrências não reproduzidas.
- Criar follow-up separado para auditoria de lifecycle SQLite no repositório.
Decisão sugerida: merge do PR #69681 após validação automática e review, seguido pelo fechamento da issue #69678 como concluída.
