* fix: sync message_count to state.db for /insights (#163) sync_session_usage() didn't write message_count to state.db, so /insights showed 0 messages for all WebUI sessions even with sync_to_insights enabled. Added message_count parameter to sync_session_usage() and pass len(s.messages) from both the streaming and non-streaming chat paths. Fixes #163 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use callable pattern for _execute_write in sync_session_usage --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
3.5 KiB
Python
114 lines
3.5 KiB
Python
"""
|
|
Hermes Web UI -- Optional state.db sync bridge.
|
|
|
|
Mirrors WebUI session metadata (token usage, title, model) into the
|
|
hermes-agent state.db so that /insights, session lists, and cost
|
|
tracking include WebUI activity.
|
|
|
|
This is opt-in via the 'sync_to_insights' setting (default: off).
|
|
All operations are wrapped in try/except -- if state.db is unavailable,
|
|
locked, or the schema doesn't match, the WebUI continues normally.
|
|
|
|
The bridge uses absolute token counts (not deltas) because the WebUI
|
|
Session object already accumulates totals across turns. This avoids
|
|
any double-counting risk.
|
|
"""
|
|
import os
|
|
from pathlib import Path
|
|
|
|
|
|
def _get_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.
|
|
Each caller is responsible for calling db.close() when done.
|
|
"""
|
|
try:
|
|
from hermes_state import SessionDB
|
|
except ImportError:
|
|
return None
|
|
|
|
try:
|
|
from api.profiles import get_active_hermes_home
|
|
hermes_home = Path(get_active_hermes_home()).expanduser().resolve()
|
|
except Exception:
|
|
hermes_home = Path(os.getenv('HERMES_HOME', str(Path.home() / '.hermes')))
|
|
|
|
db_path = hermes_home / 'state.db'
|
|
if not db_path.exists():
|
|
return None
|
|
|
|
try:
|
|
return SessionDB(db_path)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def sync_session_start(session_id: str, model=None) -> None:
|
|
"""Register a WebUI session in state.db (idempotent).
|
|
Called when a session's first message is sent.
|
|
"""
|
|
db = _get_state_db()
|
|
if not db:
|
|
return
|
|
try:
|
|
db.ensure_session(
|
|
session_id=session_id,
|
|
source='webui',
|
|
model=model,
|
|
)
|
|
except Exception:
|
|
pass # never crash the WebUI for sync failures
|
|
finally:
|
|
try:
|
|
db.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def sync_session_usage(session_id: str, input_tokens: int=0, output_tokens: int=0,
|
|
estimated_cost=None, model=None, title: str=None,
|
|
message_count: int=None) -> None:
|
|
"""Update token usage and title for a WebUI session in state.db.
|
|
Called after each turn completes. Uses absolute=True to set totals
|
|
(the WebUI Session already accumulates across turns).
|
|
"""
|
|
db = _get_state_db()
|
|
if not db:
|
|
return
|
|
try:
|
|
# Ensure session exists first (idempotent)
|
|
db.ensure_session(session_id=session_id, source='webui', model=model)
|
|
# Set absolute token counts
|
|
db.update_token_counts(
|
|
session_id=session_id,
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
estimated_cost_usd=estimated_cost,
|
|
model=model,
|
|
absolute=True,
|
|
)
|
|
# Update title if we have one, using the public API
|
|
if title:
|
|
try:
|
|
db.set_session_title(session_id, title)
|
|
except Exception:
|
|
pass
|
|
# Update message count
|
|
if message_count is not None:
|
|
try:
|
|
def _set_msg_count(conn):
|
|
conn.execute(
|
|
"UPDATE sessions SET message_count = ? WHERE id = ?",
|
|
(message_count, session_id),
|
|
)
|
|
db._execute_write(_set_msg_count)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass # never crash the WebUI for sync failures
|
|
finally:
|
|
try:
|
|
db.close()
|
|
except Exception:
|
|
pass
|