…ps (alt to #867) (#869)
Alternative to #867. This branch is built on @rodboev's commits (kept in
the history, so the original work and authorship are preserved); it changes
only how "active duration" is computed.
## What changed
#867 computes a session's active duration by summing only the gaps that
*follow* a tool-use message. That captures tool-execution latency but
discards all model generation and thinking time — including the time spent
before the first tool call — so a session that reasons for minutes and then
fires one quick tool is scored as nearly idle.
This branch instead defines active duration the same way the existing
velocity "active minutes" metric already does: the sum of consecutive
inter-message gaps, with each gap capped at 5 minutes. Every gap counts —
model generation, tool execution, and quick human turnarounds — and only
stretches longer than the cap are bounded as idle.
## Why
Message timestamps alone can't tell a 20-minute subagent run from a
20-minute coffee break; both are one long gap. Capping each gap fails
gracefully in both directions (a long idle gap and a long active gap each
contribute at most 5 minutes) instead of guessing, and it keeps the metric
robust to messy data: resumed sessions, machine sleep, and API stalls
produce the largest gaps, and those would otherwise dominate the sum.
It also unifies the definition. The 5-minute cap is hoisted to a single
shared constant (`db.ActiveGapCapSec` / `ActiveGapCapMs`) used by both the
velocity metric and Top Sessions, so the two "active" numbers on the
dashboard can't drift; a test asserts the seconds and milliseconds forms
agree.
## Tradeoff
A genuinely long active operation — a 15-minute test run, a 20-minute
subagent — is also capped at 5 minutes, so it is undercounted. That is the
deliberate cost of not guessing whether a long gap was work or idle; the
error is bounded and symmetric.
## Where to look
- `internal/db/analytics.go` — shared constant, SQLite active-duration SQL,
and the velocity metric now sourcing the same cap.
- `internal/db/timing.go` — SQLite Go fallback (timezone-aware ranking path)
running the same clamp.
- `internal/postgres/analytics.go`, `internal/duckdb/analytics_usage.go` —
PostgreSQL and DuckDB twins.
All three SQL backends drop the `has_tool_use` filter and the trailing gap
to `ended_at` so the value matches the velocity computation.
<sup>generated by a clanker</sup>
Co-authored-by: Marius van Niekerk <mariusvniekerk@users.noreply.github.com>
Top Sessions currently ranks by wall-clock duration, computed as the gap between
started_atandended_at. That overstates sessions left idle, a terminal left open overnight dominates the ranking even if actual tool work lasted only a few minutes. The underlying per-turn timing data already exists and powers the per-session timing endpoint, but the analytics aggregation path never uses it.This change adds an explicit
active_duration_minfield to the Top Sessions contract, computed from the same tool-turn timing semantics the repo already uses elsewhere, and switches the duration ranking to use that active value across the SQLite, PostgreSQL, and DuckDB analytics backends. The existingduration_minfield stays populated so total session time remains visible to users as secondary context, which keeps the “show both” part of the issue intact.On the frontend, the duration view now reads as active or tool time, uses the active value as the primary metric, keeps total duration available alongside it, and routes the new tooltip text through the existing Paraglide message surface. Focused backend regression tests cover the case where wall-clock duration and active duration disagree, the SQLite DST fallback path, DuckDB parity, PostgreSQL running-session null handling, and the ranking helper, while the Top Sessions frontend test proves the new display contract.
Fixes #83