From 25670cd9e3ab0999b8ba04ea251a899f5e94453a Mon Sep 17 00:00:00 2001 From: Mi55ed <268717603+Mi55ed@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:49:14 +0000 Subject: [PATCH] fix(state): batch large session cleanup queries below SQLite's variable limit prune_sessions(), delete_empty_sessions() and prune_empty_ghost_sessions() built one IN (?, ..., ?) clause containing every selected session id (the parent-orphaning UPDATE), so cleaning more than SQLITE_MAX_VARIABLE_NUMBER sessions failed atomically with "too many SQL variables". The per-row DELETE loops that followed are folded into the same 900-id batches. Same single _execute_write() transaction; only the binding is split. Hand-ported from PR #100658 (targeted the pre-decomposition hermes_state.py god file; the methods now live in hermes_state_maintenance.py / hermes_state_sessions.py). The one-pass transcript-directory sweep from that PR is not ported (out of scope for the variable-limit bug). Authored by @Mi55ed; ported under --author. --- hermes_state_maintenance.py | 18 ++++++++++-------- hermes_state_sessions.py | 14 ++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hermes_state_maintenance.py b/hermes_state_maintenance.py index c501102fb5..e3e86cab14 100644 --- a/hermes_state_maintenance.py +++ b/hermes_state_maintenance.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from hermes_state_common import ( - AUTO_VACUUM_MIN_FREELIST_RATIO, _placeholders, _sql_session_last_active, escape_like as _escape_like + AUTO_VACUUM_MIN_FREELIST_RATIO, _id_chunks, _placeholders, _sql_session_last_active, escape_like as _escape_like ) # caplog tests pin the "hermes_state" logger name. @@ -95,8 +95,9 @@ class SessionMaintenanceMixin: SELECT 1 FROM messages WHERE messages.session_id = sessions.id ) """, (cutoff,)).fetchall()] + for chunk in _id_chunks(ids): + conn.execute(f"DELETE FROM sessions WHERE id IN ({_placeholders(chunk)})", chunk) if ids: - conn.execute(f"DELETE FROM sessions WHERE id IN ({_placeholders(ids)})", ids) self._delete_unreferenced_system_prompts(conn) return ids removed_ids = self._execute_write(_do) or [] @@ -282,12 +283,13 @@ class SessionMaintenanceMixin: if self._write_guards_reject(conn, sid, allow_closed_compression_parent=True)} if not session_ids: return 0 - conn.execute(f"UPDATE sessions SET parent_session_id = NULL " - f"WHERE parent_session_id IN ({_placeholders(session_ids)})", list(session_ids)) - for sid in session_ids: - conn.execute("DELETE FROM messages WHERE session_id = ?", (sid,)) - conn.execute("DELETE FROM sessions WHERE id = ?", (sid,)) - removed_ids.append(sid) + # Batched: a cron-heavy store prunes tens of thousands of ids in one call. + for chunk in _id_chunks(session_ids): + ph = _placeholders(chunk) + conn.execute(f"UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id IN ({ph})", chunk) + conn.execute(f"DELETE FROM messages WHERE session_id IN ({ph})", chunk) + conn.execute(f"DELETE FROM sessions WHERE id IN ({ph})", chunk) + removed_ids.extend(chunk) self._delete_unreferenced_system_prompts(conn) return len(session_ids) count = self._execute_write(_do) diff --git a/hermes_state_sessions.py b/hermes_state_sessions.py index 5f9e3f85b8..74d2c3da8b 100644 --- a/hermes_state_sessions.py +++ b/hermes_state_sessions.py @@ -1573,16 +1573,14 @@ class SessionSessionsMixin: ).fetchall()} if not session_ids: return 0 - conn.execute( - "UPDATE sessions SET parent_session_id = NULL " - f"WHERE parent_session_id IN ({_session_ids_placeholders(session_ids)})", list(session_ids), - ) - for sid in session_ids: + for chunk in _id_chunks(session_ids): + ph = _session_ids_placeholders(chunk) + conn.execute(f"UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id IN ({ph})", chunk) # DELETE FROM messages: a row inserted between the SELECT and here # would otherwise dangle (clean FK state). - conn.execute("DELETE FROM messages WHERE session_id = ?", (sid,)) - conn.execute("DELETE FROM sessions WHERE id = ?", (sid,)) - removed_ids.append(sid) + conn.execute(f"DELETE FROM messages WHERE session_id IN ({ph})", chunk) + conn.execute(f"DELETE FROM sessions WHERE id IN ({ph})", chunk) + removed_ids.extend(chunk) self._delete_unreferenced_system_prompts(conn) return len(session_ids) count = self._execute_write(_do)