From dcf881f1e854638d717a0a84c2a8b24a9218af08 Mon Sep 17 00:00:00 2001 From: Randy Marsden Date: Wed, 5 Aug 2026 23:26:37 -0400 Subject: [PATCH] fix: close leaked state.db read-only connections in session discoverability/recovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three sites used `with sqlite3.connect(f"file:...?mode=ro", uri=True) as conn:`, which only scopes the transaction — sqlite3.Connection's context manager never calls close(). Same recurring bug class as #1421/#1494/#2233/#3904. On a long-lived server each poll leaked a state.db (+ -wal/-shm) fd until the process hit macOS's 256 soft FD limit and writes started failing. Wrap all three in contextlib.closing(), matching the pattern already used elsewhere in these files. --- api/session_discoverability.py | 3 ++- api/session_recovery.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/session_discoverability.py b/api/session_discoverability.py index 630ccdf6..671ad23c 100644 --- a/api/session_discoverability.py +++ b/api/session_discoverability.py @@ -17,6 +17,7 @@ import threading import shutil import sqlite3 from collections import Counter +from contextlib import closing from pathlib import Path from typing import Iterable @@ -102,7 +103,7 @@ def _read_state_db(state_db_path: Path | None) -> dict[str, dict]: if state_db_path is None or not state_db_path.exists(): return {} try: - with sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True) as conn: + with closing(sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True)) as conn: conn.row_factory = sqlite3.Row tables = {row[0] for row in conn.execute("select name from sqlite_master where type='table'")} if "sessions" not in tables: diff --git a/api/session_recovery.py b/api/session_recovery.py index 1081f365..43f7d05c 100644 --- a/api/session_recovery.py +++ b/api/session_recovery.py @@ -32,6 +32,7 @@ import os import shutil import sqlite3 import threading +from contextlib import closing from pathlib import Path from api.turn_journal import ( @@ -381,7 +382,7 @@ def _state_db_has_session(session_id: str, state_db_path: Path | None) -> bool: if state_db_path is None or not state_db_path.exists(): return True try: - with sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True) as conn: + with closing(sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True)) as conn: cur = conn.execute( "select 1 from sqlite_master where type='table' and name='sessions'" ) @@ -446,7 +447,7 @@ def _read_state_db_missing_sidecar_rows( if state_db_path is None or not state_db_path.exists(): return [] try: - with sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True) as conn: + with closing(sqlite3.connect(f"file:{state_db_path}?mode=ro", uri=True)) as conn: conn.row_factory = sqlite3.Row session_cols = {row[1] for row in conn.execute("PRAGMA table_info(sessions)").fetchall()} message_cols = {row[1] for row in conn.execute("PRAGMA table_info(messages)").fetchall()}