hermes - 💡(How to fix) Fix Analytics dashboard only counts sessions from the active profile, undercounting total token usage for multi-profile users

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

The web dashboard's Analytics page (Token Usage / Models) reads sessions from a single state.db — the one belonging to whichever profile launched the dashboard process. For users running multiple profiles (each with its own ~/.hermes/profiles/<name>/state.db), every other profile's usage is silently excluded. In my case the dashboard was showing only ~14% of actual token usage across 7 profiles.

Root Cause

hermes_cli/web_server.py instantiates SessionDB() with no path, so it resolves to get_hermes_home() / "state.db" for the current process's profile:

https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/web_server.py#L2803-L2869 (/api/analytics/usage) https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/web_server.py#L2873-... (/api/analytics/models)

@app.get("/api/analytics/usage")
async def get_usage_analytics(days: int = 30):
    from hermes_state import SessionDB
    ...
    db = SessionDB()       # <-- only reads current profile's state.db
    ...

The same pattern applies to /api/analytics/models. Both queries issue FROM sessions against that single DB.

hermes_state.py:

DEFAULT_DB_PATH = get_hermes_home() / "state.db"
class SessionDB:
    def __init__(self, db_path: Path = None):
        self.db_path = db_path or DEFAULT_DB_PATH

Because profiles are deliberately isolated by get_hermes_home(), there's no current mechanism for analytics to aggregate across them.

Fix Action

Workaround

Until fixed, users can get true totals by querying each state.db directly:

import sqlite3, time, glob, os
cutoff = time.time() - 30*86400
paths = [os.path.expanduser("~/.hermes/state.db")] + \
        glob.glob(os.path.expanduser("~/.hermes/profiles/*/state.db"))
for p in paths:
    c = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
    r = c.execute("SELECT SUM(input_tokens), SUM(output_tokens), COUNT(*) "
                  "FROM sessions WHERE started_at > ?", (cutoff,)).fetchone()
    print(p, r); c.close()

Code Example

@app.get("/api/analytics/usage")
async def get_usage_analytics(days: int = 30):
    from hermes_state import SessionDB
    ...
    db = SessionDB()       # <-- only reads current profile's state.db
    ...

---

DEFAULT_DB_PATH = get_hermes_home() / "state.db"
class SessionDB:
    def __init__(self, db_path: Path = None):
        self.db_path = db_path or DEFAULT_DB_PATH

---

import sqlite3, time, glob, os
cutoff = time.time() - 30*86400
paths = [os.path.expanduser("~/.hermes/state.db")] + \
        glob.glob(os.path.expanduser("~/.hermes/profiles/*/state.db"))
for p in paths:
    c = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
    r = c.execute("SELECT SUM(input_tokens), SUM(output_tokens), COUNT(*) "
                  "FROM sessions WHERE started_at > ?", (cutoff,)).fetchone()
    print(p, r); c.close()
RAW_BUFFERClick to expand / collapse

Analytics dashboard only counts sessions from the active profile, undercounting total token usage for multi-profile users

Summary

The web dashboard's Analytics page (Token Usage / Models) reads sessions from a single state.db — the one belonging to whichever profile launched the dashboard process. For users running multiple profiles (each with its own ~/.hermes/profiles/<name>/state.db), every other profile's usage is silently excluded. In my case the dashboard was showing only ~14% of actual token usage across 7 profiles.

Environment

  • Hermes: v2026.5.7-26-gfaa13e49f (main at commit faa13e49f, 2026-05-07)
  • macOS, multi-profile setup (1 default + 6 named profiles)
  • Each profile has its own state.db (verified: sizes range from 1.6 MB to 118 MB)

Repro

  1. Create 2+ profiles with hermes profile create <name> and use them enough to accumulate sessions.
  2. Launch the dashboard from the default profile (or any single profile).
  3. Open Analytics → Token Usage.
  4. Observed totals only reflect the launching profile's state.db. Other profiles' sessions, tokens, costs, and API calls are missing entirely.

Measured impact (30-day window, my setup)

profileinput tokenssessions
default66.5 M66
A29.7 M31
B28.7 M35
C8.5 M35
D308.2 M726
E23.2 M33
F1.6 M8
Total466.4 M934

Dashboard displayed only the default row (14.3% of true total).

Root cause

hermes_cli/web_server.py instantiates SessionDB() with no path, so it resolves to get_hermes_home() / "state.db" for the current process's profile:

https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/web_server.py#L2803-L2869 (/api/analytics/usage) https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/web_server.py#L2873-... (/api/analytics/models)

@app.get("/api/analytics/usage")
async def get_usage_analytics(days: int = 30):
    from hermes_state import SessionDB
    ...
    db = SessionDB()       # <-- only reads current profile's state.db
    ...

The same pattern applies to /api/analytics/models. Both queries issue FROM sessions against that single DB.

hermes_state.py:

DEFAULT_DB_PATH = get_hermes_home() / "state.db"
class SessionDB:
    def __init__(self, db_path: Path = None):
        self.db_path = db_path or DEFAULT_DB_PATH

Because profiles are deliberately isolated by get_hermes_home(), there's no current mechanism for analytics to aggregate across them.

Proposed fix

Two options, not mutually exclusive:

1. Aggregate across all profiles by default. Discover profile DBs by scanning <HERMES_HOME_ROOT>/profiles/*/state.db plus the root state.db, open each read-only, run the queries, and sum/merge results. Add a profile dimension to the returned rows so the UI can show a breakdown or let the user filter.

2. Add explicit profile filtering. /api/analytics/usage?profile=all|default|<name> with all as the new default. Preserves current behavior when a specific profile is requested.

The UI in web/src/pages/AnalyticsPage.tsx would gain a profile selector and optionally a stacked breakdown.

Edge cases worth thinking about:

  • A profile's DB being locked by a running gateway/agent — opening read-only (mode=ro&immutable=1 or PRAGMA query_only=ON after attach) avoids blocking writers.
  • sessions schema drift between profiles on different Hermes versions — already unlikely given single-repo install, but the aggregator should tolerate missing columns gracefully.
  • Privacy: users who intentionally keep profiles isolated may want opt-in aggregation via a config flag (e.g. dashboard.analytics.aggregate_profiles: true, default true).

Same fix needed for /api/analytics/models and any other analytics endpoint backed by SessionDB()/api/analytics/skills / insights engine should be checked too.

Workaround

Until fixed, users can get true totals by querying each state.db directly:

import sqlite3, time, glob, os
cutoff = time.time() - 30*86400
paths = [os.path.expanduser("~/.hermes/state.db")] + \
        glob.glob(os.path.expanduser("~/.hermes/profiles/*/state.db"))
for p in paths:
    c = sqlite3.connect(f"file:{p}?mode=ro", uri=True)
    r = c.execute("SELECT SUM(input_tokens), SUM(output_tokens), COUNT(*) "
                  "FROM sessions WHERE started_at > ?", (cutoff,)).fetchone()
    print(p, r); c.close()

Willing to help

Happy to send a PR if the maintainers agree with option 1 + 2. Want to confirm the preferred approach (aggregate-by-default vs opt-in, config flag name, whether to expose profile in the row schema) before I start.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING