Skip to content

Canonical telemetry home (HERMES_TELEMETRY_HOME)

Telemetry file locations resolve through paths.py:

  • paths.get_telemetry_home() — PURE resolver (never creates the dir), precedence:
    1. HERMES_TELEMETRY_HOME — opt-in shared cost-center dir, if set
    2. HERMES_HOME — this profile’s Hermes home
    3. ~/.hermes — default …always under a telemetry/ subdir.
  • paths.get_db_path() — telemetry.db; the ONLY getter that creates the telemetry dir (the DB writes there).
  • paths.get_budget_path() / paths.get_pricing_path() — budget.yaml / pricing.yaml; pure (no mkdir), so an absent/unwritable canonical home never turns a config read into a crash (absence of budget.yaml = “budgets disabled”).

When HERMES_TELEMETRY_HOME is set, every profile/process/cron shares one telemetry.db, one budget.yaml, and one pricing.yaml — the single-pane cost center. When unset, behavior is unchanged (each HERMES_HOME keeps its own, still profile-tagged, telemetry dir).

Governs telemetry files only. state.db and cron/ are NOT relocated — they stay on HERMES_HOME.

Package vs dashboard. The package runtime (db.py, budget.py, pricing.py) routes through paths.py (db.py/pricing.py use a try: from . import paths / except ImportError: import paths dual-mode import because some tests bare-import them). The dashboard surfaces (dashboard/plugin_api.py, dashboard/serve.py) are loaded outside the package and are code-isolated (tests/test_dashboard_plugin_isolation.py), so they replicate the precedence inline rather than importing paths.py. This PR updated serve.py (telemetry DB); plugin_api.py follows in the dashboard PR.

Dashboard profile awareness (plugin surface)

Section titled “Dashboard profile awareness (plugin surface)”

dashboard/plugin_api.py reads the canonical telemetry home (inline HERMES_TELEMETRY_HOME precedence — self-contained, no paths.py import). Its aggregate endpoints accept an optional profile query param: runs-based endpoints filter AND profile = ?; llm_calls-based endpoints filter AND session_id IN (SELECT session_id FROM runs WHERE profile = ?) (a correlated subquery — avoids a JOIN and the runs/llm_calls column-name ambiguity). /profiles returns the distinct non-null profiles. The dist/index.js TelemetryPage renders a selector (shown only when profiles exist) that threads &profile= into each panel. Shared-mode only — there is no cross-DB reading (consolidate via HERMES_TELEMETRY_HOME first). /budget and the slot widgets are not profile-filtered.

Gotcha — no X | None on FastAPI route params. The profile query param is declared profile: str = "" (not str | None = None). FastAPI evaluates route-parameter annotations at decoration time (get_type_hints), and str | None raises TypeError on Python 3.8/3.9 (this repo targets >=3.8) — even with from __future__ import annotations. Optional[str] avoids that but ruff UP045 would demand X | None back. str = "" is annotated, ruff-clean, cross-version-safe, and falsy-when-absent (the helpers gate on if profile:), so it’s the correct shape for optional query params in this file.

Points every Hermes profile at one shared telemetry home so all profiles read the same pricing.yaml / budget.yaml and write to the same telemetry.db (the consolidation HERMES_TELEMETRY_HOME enables). Lives in sync_profiles.py (self-contained, no intra-package imports) + a subcommand in telemetry_cli.py (_handle_sync_profiles). No schema change.

Verified per-profile model (source: NousResearch/hermes-agent@main, 2026-07-08): each profile is an independent HERMES_HOME at ~/.hermes/profiles/<name>/ (the default profile is ~/.hermes), with its own config.yaml, .env, and plugins/. Hermes core loads <home>/.env via python-dotenv (override=True) after resolving the profile and BEFORE loading plugins, so HERMES_TELEMETRY_HOME written there reaches that profile’s process — no user-side sourcing.

Terminal window
hermes telemetry sync-profiles # dry-run (default): shows the plan, mutates nothing
hermes telemetry sync-profiles --apply # RUN FROM THE DEFAULT PROFILE (shared home = ~/.hermes)
  • The ONLY file mutated is each profile’s .env — a single HERMES_TELEMETRY_HOME line, written atomically (tmp + os.replace). Idempotent; other lines and comments are kept and an existing export prefix on the key is preserved (line endings are normalized to \n).
  • config.yaml is read-only: the command warns when a profile has not enabled the plugin (plugins.enabled) but NEVER edits it. Auto-enable and plugin-symlinking were deliberately dropped — the repo has no comment-preserving YAML writer, and enabling is a one-line manual step that sync-profiles prints correctly: hermes plugins install hermes-telemetry -p <name> --enable if not installed, else hermes plugins enable hermes-telemetry -p <name>.
  • Non-default guard: enumeration is default ~/.hermes + ~/.hermes/profiles/*/; the target defaults to the current resolved home (HERMES_TELEMETRY_HOME > HERMES_HOME > ~/.hermes). If run from a named profile, --apply refuses unless --yes is passed — review the dry-run first. The profile that already is the shared home is skipped. (Enumeration is always rooted at the invoking home, so running from a named profile only ever sees that profile’s own subtree — another reason to run from the default profile.)
  • Flags: --telemetry-home PATH overrides the shared home; [names...] limits to specific profiles; --json emits a machine-readable report.
  • Going-forward only: it does not backfill telemetry rows already siloed in a profile’s own pre-consolidation DB.

hermes-telemetry is an independent community plugin for Hermes Agent. It is not affiliated with or endorsed by Nous Research.