Skip to content

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

Merged
nesquena-hermes merged 1 commit intomasterfrom
feat/state-db-sync-opt-in
Apr 5, 2026
Merged

feat: opt-in state.db sync for /insights visibility (#92)#93
nesquena-hermes merged 1 commit intomasterfrom
feat/state-db-sync-opt-in

Conversation

@nesquena
Copy link
Copy Markdown
Owner

@nesquena nesquena commented Apr 5, 2026

Summary

Fixes #92 -- WebUI sessions are invisible to hermes /insights because the WebUI calls AIAgent.run_conversation() directly, bypassing the gateway that normally writes to state.db.

Approach

New opt-in setting "Sync usage to /insights" (default: off). When enabled, after each turn completes the WebUI mirrors session metadata (tokens, cost, model, title) into state.db so /insights includes browser session data.

Why default off?

  • Writing to state.db while CLI/gateway also writes could cause WAL lock contention
  • Some users may not want WebUI sessions polluting their /insights stats
  • It's a one-way mirror (WebUI -> state.db), not full bidirectional sync

Components

File Change
api/state_sync.py NEW -- bridge module: sync_session_usage() using ensure_session() + update_token_counts(absolute=True)
api/config.py sync_to_insights boolean setting added to defaults
api/streaming.py Calls sync_session_usage() after s.save() (streaming path)
api/routes.py Same for non-streaming chat path
static/index.html Checkbox in Settings panel
static/panels.js Load/save the checkbox state

Safety

  • All sync operations wrapped in try/except -- WebUI never crashes for sync failures
  • Uses absolute=True on update_token_counts() -- the WebUI Session already accumulates totals, so no double-counting risk
  • ensure_session() is idempotent (INSERT OR IGNORE)
  • If hermes_state module is not importable or state.db doesn't exist, sync silently no-ops

Test plan

  • 400 passed, 24 skipped, zero failures
  • Enable "Sync usage to /insights" in Settings
  • Send a message, then run hermes /insights -- should show the WebUI session
  • Disable the setting, send another message -- /insights should not update
  • With hermes-agent not installed: setting enabled but sync silently skips

Generated with Claude Code

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) <[email protected]>
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Agent review — APPROVED WITH FIXES ✅

Full diff review, security audit, correctness audit against the live hermes-agent codebase, and test run completed.

What this PR does: Adds an opt-in setting ("Sync usage to /insights") that mirrors WebUI session token usage and titles into state.db after each turn, so hermes /insights includes browser session activity. Default off. All sync ops wrapped in try/except so a sync failure never crashes the WebUI.

Diff: 6 files, 134 insertions, 1 deletion
Security: CLEAN
Tests: 424 passed, 0 failed

Fixes applied on pr-93-review branch:

Three bugs found by auditing the actual hermes_state.py module signatures:

  1. Wrong class name (blocker). api/state_sync.py imported HermesState but the class is named SessionDB. The ImportError was caught silently, so the feature would have been a complete no-op on every real installation. Fixed: from hermes_state import SessionDB.

  2. Wrong type passed to constructor (blocker). SessionDB.__init__ takes a Path object; the original code passed str(db_path). Path.parent doesn't exist on a str, so instantiation would crash with AttributeError. Fixed: SessionDB(db_path) (no str conversion).

  3. Wrong _execute_write() call signature (blocker). The original code called db._execute_write("UPDATE ...", (params,)) treating it like a direct SQL runner. The actual signature is _execute_write(fn: Callable[[Connection], T]) -- it takes a callable, not SQL strings. Also a private method. Replaced with the public db.set_session_title(session_id, title) which exists exactly for this purpose.

  4. Connection leak (medium). _get_state_db() opened a persistent SQLite connection on every call and never closed it. Under WAL mode this prevents checkpointing and the WAL file grows unboundedly. Fixed: added try/finally db.close() in both sync_session_start() and sync_session_usage().

The rest of the design is solid: update_token_counts(absolute=True) signature matches exactly, ensure_session(source=, model=) matches, the title column exists in state.db, and get_active_hermes_home() routes correctly per active profile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebUI sessions are invisible to /insights

2 participants