relatorio-issue-69678-sqlite-fd-leaks.md
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
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.
| 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.
Os módulos usam o seguinte padrão:
with _connect() as conn:
...
O context manager de sqlite3.Connection controla a transação:
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:
-wal;-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.
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:
with conn:;finally;O PR #69681 aplica o mesmo modelo a três ledgers não alterados pelo PR #69594.
Não marcar #69678 como duplicata de #69567.
Justificativa:
Classificação correta: issues irmãs pertencentes à mesma classe de defeito.
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.
_transaction() não adquire _DB_LOCK, evitando lock nesting novo;_prune() em delivery_ledger continua lock-free;Nenhum defeito funcional foi identificado no patch analisado.
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:
_transaction();O volume +512/-29 vem principalmente dos três arquivos de regressão. O fix runtime em si permanece cirúrgico.
O PR adiciona testes para:
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.
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
Mesma classe geral de lifecycle SQLite, mas escopos diferentes:
SessionDB em early return;ResponseStore no API server;response_store.db.Essas issues demonstram padrão recorrente: uso de context manager transacional interpretado incorretamente como gerenciamento completo da conexão.
Busca estática encontrou padrões semelhantes fora do escopo do PR, incluindo:
gateway/readiness.py;optional-skills/mcp/fastmcp/templates/database_server.py.Esses locais não devem ser incluídos automaticamente em #69681. Cada ocorrência precisa de:
Expandir #69681 para uma auditoria global contrariaria objetivo de mudança cirúrgica.
Após merge dos fixes urgentes, abrir tarefa separada de auditoria dirigida:
with sqlite3.connect(...), with connect(...) e with _connect(...);contextlib.closing, try/finally close() ou helper transacional para conexões per-operation;sqlite3.Connection context 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.
Decisão sugerida: merge do PR #69681 após validação automática e review, seguido pelo fechamento da issue #69678 como concluída.