HERMES-GIELIE/api
Hermes Bot 51a87ebdc7 fix(sqlite): close state.db connections explicitly to stop FD leak in sidebar polling (#1494)
Production WebUI on macOS launchd reproduced an HTTP-unhealthy wedge after
#1483 closed the bootstrap supervisor double-fork: process alive, port
listening, every HTTP request reset by peer before a response. The reporter
(@insecurejezza) traced it to FD exhaustion — 366 open FDs on the wedged
process, 238 of them `~/.hermes/state.db`, `state.db-wal`, and `state.db-shm`.

Root cause: four sqlite callsites use `with sqlite3.connect(...) as conn:`.
Python's sqlite3 connection context manager only commits or rolls back on
exit; it does NOT close the connection. `/api/sessions` polling calls these
on every sidebar refresh, so each poll leaked one or more open state.db FDs
until the process hit macOS's soft FD limit and new sqlite3.connect() calls
inside fresh request handlers raised before any response bytes were written.

Fix: wrap each `sqlite3.connect(...)` in `contextlib.closing(...)` so the
connection is explicitly closed on scope exit, in addition to the auto-
commit / rollback semantics that `Connection.__exit__` already provides.

Callsites patched:
- api/agent_sessions.py:read_importable_agent_session_rows
- api/agent_sessions.py:read_session_lineage_metadata
- api/models.py:get_cli_session_messages
- api/models.py:delete_cli_session

Reporter's verification (post-patch, 100-request stress loop against
/api/sessions and /api/projects):

  batch=1 fd=92 state_handles=0
  batch=2 fd=92 state_handles=0
  ...
  batch=5 fd=92 state_handles=0

Pre-patch the same loop made FD count and state.db handle count climb
monotonically.

4 regression tests in tests/test_issue1494_state_db_fd_leak.py monkeypatch
sqlite3.connect with a tracking wrapper that records .close() calls and
assert every connection opened by each of the four functions is explicitly
closed. Verified to fail (catching the original bug) when the closing()
wrap is reverted: "leaked 5 of 5 sqlite connection(s) — context-manager-
only `with sqlite3.connect()` does not close. Wrap in contextlib.closing()."

This addresses Bug #2 of the umbrella issue #1458. Bug #3 (HTTP-unhealthy
wedge in the absence of FD exhaustion) remains open pending separate
diagnostic data — explicit scope discipline.

Closes #1494
Refs #1458 (Bug #2 of 3)

Co-authored-by: insecurejezza <70424851+insecurejezza@users.noreply.github.com>
2026-05-03 01:15:26 +00:00
..
__init__.py Hermes Web UI — Sprints 11-14: multi-provider models, settings, session QoL, alerts, polish 2026-03-31 07:02:47 +00:00
agent_sessions.py fix(sqlite): close state.db connections explicitly to stop FD leak in sidebar polling (#1494) 2026-05-03 01:15:26 +00:00
auth.py feature: add manifest and icons to enable app install on android 2026-05-02 19:06:39 +02:00
background.py feat(commands): /background, /btw slash commands + undo button + reasoning chip 2026-04-24 01:24:51 +00:00
clarify.py feat(clarify): SSE long-connection for real-time clarify notifications (#1355) 2026-04-30 21:32:51 +00:00
commands.py feat: slash command parity + skill autocomplete — v0.50.91 (PR #711) 2026-04-19 05:37:44 +00:00
config.py chore(release): stamp v0.50.267 — 7 contributor PR batch + Opus follow-up 2026-05-02 17:03:25 +00:00
extensions.py Opus pre-release follow-ups for PR #1445 2026-05-02 03:49:40 +00:00
gateway_watcher.py release: v0.50.241 (#1293) 2026-04-29 19:54:07 -07:00
helpers.py v0.50.255: Opus follow-ups (4 fixes) + CHANGELOG 2026-05-01 17:19:53 +00:00
metering.py v0.50.207: batch of 10 PRs — TPS stat, SSE guard, session polish, cron UX, folder create, model errors, session speed, title gen (#1031) 2026-04-25 13:07:35 -07:00
models.py fix(sqlite): close state.db connections explicitly to stop FD leak in sidebar polling (#1494) 2026-05-03 01:15:26 +00:00
oauth.py v0.50.257: 4 Opus pre-release follow-ups + CHANGELOG + test fixes for #1415 2026-05-01 18:30:41 +00:00
onboarding.py absorb: address Opus review findings (security + correctness) 2026-04-29 05:06:34 +00:00
profiles.py fix(cancel-stream): rename tool_calls to _partial_tool_calls (Opus MUST-FIX) 2026-04-30 23:43:23 +00:00
providers.py fix: TTS toggle CSS specificity collision (#1409) + Ollama env var bleed (#1410) 2026-05-01 17:14:51 +00:00
rollback.py v0.50.255: Opus follow-ups (4 fixes) + CHANGELOG 2026-05-01 17:19:53 +00:00
routes.py chore(release): stamp v0.50.268 — 4-PR batch + Opus follow-ups (i18n + per-session fields + None title guard) 2026-05-02 17:54:58 +00:00
session_ops.py Show profile home in /status command (refs #463) (#1380) 2026-05-01 04:46:37 +00:00
startup.py fix: P0 hotfixes — API regression, code block parser, chmod override 2026-05-01 12:10:48 +00:00
state_sync.py security: bandit fixes B310/B324/B110 + QuietHTTPServer (#354) 2026-04-13 11:11:56 -07:00
streaming.py v0.50.259: Opus pre-release follow-up — close _session_db on LRU eviction + CHANGELOG + 5 regression tests 2026-05-01 22:42:53 +00:00
terminal.py absorb: address Opus review findings (security + correctness) 2026-04-29 05:06:34 +00:00
updates.py fix: update banner — conflict recovery path + server self-restart after update (#816) 2026-04-21 17:10:41 -07:00
upload.py Merge remote-tracking branch 'refs/remotes/pr/1229' into stage/batch-v0.50.238 2026-04-29 15:17:57 +00:00
workspace.py fix: batch v0.50.234-235 — XSS hardening, workspace validation, profile switch fixes (#1206) 2026-04-27 21:39:30 -07:00