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.
This commit is contained in:
Mi55ed
2026-09-01 19:49:14 +00:00
committed by Teknium
parent acfda37598
commit 25670cd9e3
2 changed files with 16 additions and 16 deletions
+10 -8
View File
@@ -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)
+6 -8
View File
@@ -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)