Merge pull request #3204 from nesquena/release/stage-batch56

Release stage-batch56 → v0.51.174 (CLI/gateway session usage in Insights, double-count fix)
This commit is contained in:
nesquena-hermes authored and GitHub committed 2026-05-30 12:11:37 -07:00
commit 05122e263d
3 files changed
+156

No files matched your search

+6
View File
@@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.174] — 2026-05-30 — Release ET (stage-batch56 — CLI/gateway session usage in Insights)
### Added
- The Insights page now includes CLI and gateway sessions (Telegram, Discord, cron, TUI) from the Hermes agent's `state.db` in usage totals, model breakdown, and daily activity — not just WebUI-native sessions (#3189). WebUI sessions are de-duplicated so they are counted once, not double-counted against their `state.db` row.
## [v0.51.173] — 2026-05-30 — Release ES (stage-batch55 — Windows path/journal safety + pin-quota snapshot fix + tool-card paging anchor + sidebar dedupe + quieter tool cards)
### Fixed
+57
View File
@@ -3539,6 +3539,63 @@ def _handle_insights(handler, parsed) -> bool:
except Exception:
pass
# ── Also include CLI sessions from Hermes state.db ─────────────────────
try:
from api.models import _active_state_db_path
db_path = _active_state_db_path()
if db_path and db_path.exists():
with closing(sqlite3.connect(str(db_path))) as conn:
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("""
SELECT id, model, message_count, input_tokens, output_tokens,
estimated_cost_usd, started_at, ended_at
FROM sessions
WHERE (started_at >= ? OR ended_at >= ?)
AND COALESCE(source, '') != 'webui'
""", (cutoff, cutoff))
for row in cur.fetchall():
_input = _safe_usage_int(row["input_tokens"])
_output = _safe_usage_int(row["output_tokens"])
_cost = _safe_cost_float(row["estimated_cost_usd"])
_msgs = _safe_usage_int(row["message_count"])
total_sessions += 1
total_messages += _msgs
total_input_tokens += _input
total_output_tokens += _output
total_cost += _cost
_model = row["model"] or "unknown"
bucket = model_stats.setdefault(_model, {
"sessions": 0,
"input_tokens": 0,
"output_tokens": 0,
"cost": 0.0,
})
bucket["sessions"] += 1
bucket["input_tokens"] += _input
bucket["output_tokens"] += _output
bucket["cost"] += _cost
_ts = row["started_at"] or row["ended_at"] or 0
if _ts:
_dt = _time.localtime(_ts)
_day_key = _time.strftime("%Y-%m-%d", _dt)
_daily = daily_tokens.setdefault(_day_key, {
"input_tokens": 0,
"output_tokens": 0,
"sessions": 0,
"cost": 0.0,
})
_daily["input_tokens"] += _input
_daily["output_tokens"] += _output
_daily["sessions"] += 1
_daily["cost"] += _cost
dow_activity[_dt.tm_wday] += 1
hod_activity[_dt.tm_hour] += 1
except Exception:
logger.debug("Failed to include CLI sessions in insights", exc_info=True)
# Build model breakdown
total_tokens = total_input_tokens + total_output_tokens
models_breakdown = []
+93
View File
@@ -318,3 +318,96 @@ def test_insights_mobile_models_table_has_contained_overflow():
section_block = STYLE_CSS[section_start:section_end]
assert 'overflow-x' in section_block, 'Mobile rule should include overflow-x handling'
assert 'insights-model-table' in section_block or 'insights-card' in section_block
# ── #3189: CLI/gateway sessions in Insights + webui double-count guard ──────
def _call_insights_with_state_db(monkeypatch, tmp_path, entries, state_rows, days="7", now=None):
"""Like _call_insights but also seeds an agent state.db with `sessions` rows
and points _active_state_db_path at it, so the CLI second-pass is exercised."""
import sqlite3
import api.routes as routes
import api.models as models
session_dir = tmp_path / "sessions"
session_dir.mkdir()
(session_dir / "_index.json").write_text(json.dumps(entries), encoding="utf-8")
monkeypatch.setattr(routes, "SESSION_DIR", session_dir)
if now is not None:
monkeypatch.setattr(time, "time", lambda: now)
db_path = tmp_path / "state.db"
conn = sqlite3.connect(str(db_path))
conn.execute(
"""CREATE TABLE sessions (
id TEXT PRIMARY KEY, source TEXT, model TEXT, message_count INTEGER,
input_tokens INTEGER, output_tokens INTEGER, estimated_cost_usd REAL,
started_at REAL, ended_at REAL
)"""
)
for r in state_rows:
conn.execute(
"INSERT INTO sessions (id, source, model, message_count, input_tokens, "
"output_tokens, estimated_cost_usd, started_at, ended_at) "
"VALUES (?,?,?,?,?,?,?,?,?)",
(r["id"], r.get("source"), r.get("model"), r.get("message_count", 0),
r.get("input_tokens", 0), r.get("output_tokens", 0),
r.get("estimated_cost_usd", 0.0), r.get("started_at"), r.get("ended_at")),
)
conn.commit()
conn.close()
# _handle_insights does `from api.models import _active_state_db_path`, so patch on the module.
monkeypatch.setattr(models, "_active_state_db_path", lambda: db_path)
handler = _FakeHandler()
parsed = SimpleNamespace(query=f"days={days}")
routes._handle_insights(handler, parsed)
assert handler.status == 200
return handler.json_body()
def test_insights_includes_cli_and_gateway_sessions(monkeypatch, tmp_path):
"""CLI / Telegram / Discord sessions in state.db should appear in Insights totals."""
now = time.mktime((2026, 5, 30, 12, 0, 0, 0, 0, -1))
webui_entries = [
{"session_id": "w1", "updated_at": now, "created_at": now, "message_count": 2,
"input_tokens": 100, "output_tokens": 50, "estimated_cost": 0.01, "model": "gpt-5.5"},
]
state_rows = [
{"id": "cli1", "source": "cli", "model": "gpt-5.5", "message_count": 3,
"input_tokens": 200, "output_tokens": 80, "estimated_cost_usd": 0.02,
"started_at": now, "ended_at": now},
{"id": "tg1", "source": "telegram", "model": "gpt-5.5", "message_count": 1,
"input_tokens": 60, "output_tokens": 20, "estimated_cost_usd": 0.005,
"started_at": now, "ended_at": now},
]
data = _call_insights_with_state_db(monkeypatch, tmp_path, webui_entries, state_rows, days="7", now=now)
# 1 webui + 2 cli/gateway = 3 sessions counted
assert data["total_sessions"] == 3
# input tokens summed across all three: 100 + 200 + 60 = 360
assert data["total_input_tokens"] == 360
def test_insights_does_not_double_count_webui_state_db_rows(monkeypatch, tmp_path):
"""WebUI sessions persist to BOTH _index.json AND state.db (source='webui').
The CLI second-pass must exclude source='webui' so they aren't counted twice."""
now = time.mktime((2026, 5, 30, 12, 0, 0, 0, 0, -1))
webui_entries = [
{"session_id": "w1", "updated_at": now, "created_at": now, "message_count": 2,
"input_tokens": 100, "output_tokens": 50, "estimated_cost": 0.01, "model": "gpt-5.5"},
]
# The SAME webui session also has a state.db row (source='webui') + one real cli row.
state_rows = [
{"id": "w1", "source": "webui", "model": "gpt-5.5", "message_count": 2,
"input_tokens": 100, "output_tokens": 50, "estimated_cost_usd": 0.01,
"started_at": now, "ended_at": now},
{"id": "cli1", "source": "cli", "model": "gpt-5.5", "message_count": 3,
"input_tokens": 200, "output_tokens": 80, "estimated_cost_usd": 0.02,
"started_at": now, "ended_at": now},
]
data = _call_insights_with_state_db(monkeypatch, tmp_path, webui_entries, state_rows, days="7", now=now)
# webui session counted once (from _index.json), cli once = 2, NOT 3.
assert data["total_sessions"] == 2
# webui tokens counted once (100) + cli (200) = 300, NOT 400.
assert data["total_input_tokens"] == 300