Merge pull request #6831 from nesquena/stage/6823-fd-leak

Release exp-v0.52.167: close leaked state.db read-only connections (#6823)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-08-07 14:40:27 -07:00
commit a4bd54ef71
3 files changed
+7 -3

No files matched your search

+2
View File
@@ -21,6 +21,8 @@
### Fixed
- **Session recovery/discoverability polling no longer leaks SQLite file descriptors.** Three read-only `state.db` probes in the session recovery and discoverability paths opened their connections with a bare `with sqlite3.connect(...) as conn:` block — but SQLite's connection context manager only manages the transaction, it does not close the connection, so each poll cycle left the connection (and its file descriptor) open until the cyclic garbage collector eventually reclaimed it. Under repeated background polling this could exhaust the process file-descriptor limit (notably on macOS) and then block further session/file writes. Each probe now wraps the connection in `contextlib.closing(...)` so the descriptor is released deterministically at block exit; the read logic is otherwise unchanged. Thanks @randmars. (#6823)
- **The memory panel now honors `memory_enabled` and `user_profile_enabled`.** The Settings → Memory read/write endpoints previously loaded and saved `MEMORY.md` / `USER.md` regardless of those config flags, so a profile that disabled memory or the user profile still exposed and mutated the files. Both endpoints now read the flags from the per-profile config snapshot (`config.memory.memory_enabled` / `config.memory.user_profile_enabled`): a disabled section is omitted from reads and rejected with a 403 on write, while the default-on behavior is preserved when the flags are absent. Thanks @webtecnica. (#6498, #6406)
- **When a turn hits the tool-call limit, its final written answer now renders alongside the limit status card instead of being replaced by it.** In the ordinary (non-transparent) transcript branch, the presence of the tool-limit status card was counted as the turn's "visible body," so a genuine final assistant report was suppressed and the user saw only the status card (whose own subtitle promises "the written answer below"). The status card no longer counts as the visible body, so a non-empty report renders together with the card and picks up the standard message footer/actions; status-only turns (no report) still show just the card, and card ordering is unchanged. Thanks @rodboev. (#6486, #6467)
+2 -1
View File
@@ -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:
+3 -2
View File
@@ -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()}