fix: state_sync.py -- correct class name, constructor type, title API, connection leak

Three bugs found during review:
1. Class is SessionDB not HermesState -- would silently no-op on every install
2. SessionDB.__init__ takes Path not str -- would crash with AttributeError
3. _execute_write() takes a callable not SQL+params -- wrong signature.
   Replaced with public set_session_title() API.
4. Each call opened a persistent SQLite connection and never closed it.
   Added try/finally db.close() to prevent WAL leak under sustained load.

Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
This commit is contained in:
nesquena-hermes
2026-04-04 20:08:20 -07:00
committed by GitHub
parent bb595afde9
commit c312dd36ca

View File

@@ -18,11 +18,12 @@ from pathlib import Path
def _get_state_db(): def _get_state_db():
"""Get a HermesState instance for the active profile's state.db. """Get a SessionDB instance for the active profile's state.db.
Returns None if hermes_state is not importable or DB is unavailable. Returns None if hermes_state is not importable or DB is unavailable.
Each caller is responsible for calling db.close() when done.
""" """
try: try:
from hermes_state import HermesState from hermes_state import SessionDB
except ImportError: except ImportError:
return None return None
@@ -37,7 +38,7 @@ def _get_state_db():
return None return None
try: try:
return HermesState(str(db_path)) return SessionDB(db_path)
except Exception: except Exception:
return None return None
@@ -46,10 +47,10 @@ def sync_session_start(session_id, model=None):
"""Register a WebUI session in state.db (idempotent). """Register a WebUI session in state.db (idempotent).
Called when a session's first message is sent. Called when a session's first message is sent.
""" """
try:
db = _get_state_db() db = _get_state_db()
if not db: if not db:
return return
try:
db.ensure_session( db.ensure_session(
session_id=session_id, session_id=session_id,
source='webui', source='webui',
@@ -57,6 +58,11 @@ def sync_session_start(session_id, model=None):
) )
except Exception: except Exception:
pass # never crash the WebUI for sync failures pass # never crash the WebUI for sync failures
finally:
try:
db.close()
except Exception:
pass
def sync_session_usage(session_id, input_tokens=0, output_tokens=0, def sync_session_usage(session_id, input_tokens=0, output_tokens=0,
@@ -65,10 +71,10 @@ def sync_session_usage(session_id, input_tokens=0, output_tokens=0,
Called after each turn completes. Uses absolute=True to set totals Called after each turn completes. Uses absolute=True to set totals
(the WebUI Session already accumulates across turns). (the WebUI Session already accumulates across turns).
""" """
try:
db = _get_state_db() db = _get_state_db()
if not db: if not db:
return return
try:
# Ensure session exists first (idempotent) # Ensure session exists first (idempotent)
db.ensure_session(session_id=session_id, source='webui', model=model) db.ensure_session(session_id=session_id, source='webui', model=model)
# Set absolute token counts # Set absolute token counts
@@ -80,14 +86,16 @@ def sync_session_usage(session_id, input_tokens=0, output_tokens=0,
model=model, model=model,
absolute=True, absolute=True,
) )
# Update title if we have one # Update title if we have one, using the public API
if title: if title:
try: try:
db._execute_write( db.set_session_title(session_id, title)
"UPDATE sessions SET title = ? WHERE id = ?",
(title, session_id),
)
except Exception: except Exception:
pass pass
except Exception: except Exception:
pass # never crash the WebUI for sync failures pass # never crash the WebUI for sync failures
finally:
try:
db.close()
except Exception:
pass