Canonical telemetry home (HERMES_TELEMETRY_HOME)
Telemetry file locations resolve through paths.py:
paths.get_telemetry_home()— PURE resolver (never creates the dir), precedence:HERMES_TELEMETRY_HOME— opt-in shared cost-center dir, if setHERMES_HOME— this profile’s Hermes home~/.hermes— default …always under atelemetry/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 ofbudget.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.
Command: telemetry sync-profiles
Section titled “Command: telemetry sync-profiles”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.
hermes telemetry sync-profiles # dry-run (default): shows the plan, mutates nothinghermes telemetry sync-profiles --apply # RUN FROM THE DEFAULT PROFILE (shared home = ~/.hermes)- The ONLY file mutated is each profile’s
.env— a singleHERMES_TELEMETRY_HOMEline, written atomically (tmp +os.replace). Idempotent; other lines and comments are kept and an existingexportprefix on the key is preserved (line endings are normalized to\n). config.yamlis 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> --enableif not installed, elsehermes 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,--applyrefuses unless--yesis 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 PATHoverrides the shared home;[names...]limits to specific profiles;--jsonemits 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.