feat: opt-in state.db sync for /insights visibility (#92)

WebUI sessions were invisible to 'hermes /insights' because the WebUI
bypasses the gateway and calls AIAgent.run_conversation() directly,
never writing to state.db.

New 'Sync usage to /insights' setting (default: off) that mirrors
WebUI session metadata (tokens, cost, model, title) into state.db
after each turn. Uses absolute token counts to avoid double-counting.

Components:
- api/state_sync.py: bridge module with sync_session_start() and
  sync_session_usage(). Uses ensure_session() (idempotent) and
  update_token_counts(absolute=True). All wrapped in try/except.
- api/config.py: new 'sync_to_insights' boolean setting
- api/streaming.py: calls sync_session_usage() after s.save()
- api/routes.py: same for the non-streaming chat path
- Settings UI: checkbox toggle with description

Default off because:
- Writing to state.db while CLI/gateway also writes could cause
  WAL lock contention on busy systems
- Some users may not want WebUI sessions in /insights stats

Closes #92

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-04 20:07:05 -07:00
committed by GitHub
parent e0d52f39dc
commit bb595afde9
6 changed files with 134 additions and 1 deletions

View File

@@ -962,6 +962,8 @@ async function loadSettingsPanel(){
if(showUsageCb) showUsageCb.checked=!!settings.show_token_usage;
const showCliCb=$('settingsShowCliSessions');
if(showCliCb) showCliCb.checked=!!settings.show_cli_sessions;
const syncCb=$('settingsSyncInsights');
if(syncCb) syncCb.checked=!!settings.sync_to_insights;
// Password field: always blank (we don't send hash back)
const pwField=$('settingsPassword');
if(pwField) pwField.value='';
@@ -992,6 +994,7 @@ async function saveSettings(){
if(sendKey) body.send_key=sendKey;
body.show_token_usage=showTokenUsage;
body.show_cli_sessions=showCliSessions;
body.sync_to_insights=!!($('settingsSyncInsights')||{}).checked;
// Password: only act if the field has content; blank = leave auth unchanged
if(pw && pw.trim()){
try{