From efa778a0ef7a2537d2a34151482de5a2785179af Mon Sep 17 00:00:00 2001 From: teknium1 Date: Tue, 17 Mar 2026 03:50:06 -0700 Subject: [PATCH] fix(state): add missing thread locks to 4 SessionDB methods search_sessions(), clear_messages(), delete_session(), and prune_sessions() all accessed self._conn without acquiring self._lock. Every other method in the class uses the lock. In multi-threaded contexts (gateway serving concurrent platform messages), these unprotected methods can cause sqlite3.ProgrammingError from concurrent cursor operations on the same connection. --- hermes_state.py | 92 ++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index e990cbbc8..396c4dbf9 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -809,17 +809,18 @@ class SessionDB: offset: int = 0, ) -> List[Dict[str, Any]]: """List sessions, optionally filtered by source.""" - if source: - cursor = self._conn.execute( - "SELECT * FROM sessions WHERE source = ? ORDER BY started_at DESC LIMIT ? OFFSET ?", - (source, limit, offset), - ) - else: - cursor = self._conn.execute( - "SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?", - (limit, offset), - ) - return [dict(row) for row in cursor.fetchall()] + with self._lock: + if source: + cursor = self._conn.execute( + "SELECT * FROM sessions WHERE source = ? ORDER BY started_at DESC LIMIT ? OFFSET ?", + (source, limit, offset), + ) + else: + cursor = self._conn.execute( + "SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?", + (limit, offset), + ) + return [dict(row) for row in cursor.fetchall()] # ========================================================================= # Utility @@ -871,26 +872,28 @@ class SessionDB: def clear_messages(self, session_id: str) -> None: """Delete all messages for a session and reset its counters.""" - self._conn.execute( - "DELETE FROM messages WHERE session_id = ?", (session_id,) - ) - self._conn.execute( - "UPDATE sessions SET message_count = 0, tool_call_count = 0 WHERE id = ?", - (session_id,), - ) - self._conn.commit() + with self._lock: + self._conn.execute( + "DELETE FROM messages WHERE session_id = ?", (session_id,) + ) + self._conn.execute( + "UPDATE sessions SET message_count = 0, tool_call_count = 0 WHERE id = ?", + (session_id,), + ) + self._conn.commit() def delete_session(self, session_id: str) -> bool: """Delete a session and all its messages. Returns True if found.""" - cursor = self._conn.execute( - "SELECT COUNT(*) FROM sessions WHERE id = ?", (session_id,) - ) - if cursor.fetchone()[0] == 0: - return False - self._conn.execute("DELETE FROM messages WHERE session_id = ?", (session_id,)) - self._conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) - self._conn.commit() - return True + with self._lock: + cursor = self._conn.execute( + "SELECT COUNT(*) FROM sessions WHERE id = ?", (session_id,) + ) + if cursor.fetchone()[0] == 0: + return False + self._conn.execute("DELETE FROM messages WHERE session_id = ?", (session_id,)) + self._conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) + self._conn.commit() + return True def prune_sessions(self, older_than_days: int = 90, source: str = None) -> int: """ @@ -900,22 +903,23 @@ class SessionDB: import time as _time cutoff = _time.time() - (older_than_days * 86400) - if source: - cursor = self._conn.execute( - """SELECT id FROM sessions - WHERE started_at < ? AND ended_at IS NOT NULL AND source = ?""", - (cutoff, source), - ) - else: - cursor = self._conn.execute( - "SELECT id FROM sessions WHERE started_at < ? AND ended_at IS NOT NULL", - (cutoff,), - ) - session_ids = [row["id"] for row in cursor.fetchall()] + with self._lock: + if source: + cursor = self._conn.execute( + """SELECT id FROM sessions + WHERE started_at < ? AND ended_at IS NOT NULL AND source = ?""", + (cutoff, source), + ) + else: + cursor = self._conn.execute( + "SELECT id FROM sessions WHERE started_at < ? AND ended_at IS NOT NULL", + (cutoff,), + ) + session_ids = [row["id"] for row in cursor.fetchall()] - for sid in session_ids: - self._conn.execute("DELETE FROM messages WHERE session_id = ?", (sid,)) - self._conn.execute("DELETE FROM sessions WHERE id = ?", (sid,)) + for sid in session_ids: + self._conn.execute("DELETE FROM messages WHERE session_id = ?", (sid,)) + self._conn.execute("DELETE FROM sessions WHERE id = ?", (sid,)) - self._conn.commit() + self._conn.commit() return len(session_ids)