Session Analytics and Statistics#
Decant's analytics layer queries the local SQLite archive and is governed by three core rules: inclusive UTC date filtering, root-session-only session counts with inclusive subagent metric aggregation, and recursive subagent cost summation in per-session summaries. The authoritative implementation is split across src/date-filter.ts, src/stats.ts, src/query.ts, and src/ui/session-summary.ts. The methodology is documented in docs/analytics-methodology.md and practical query patterns are in docs/api/recipes.md.
Date Filtering#
Date ranges are applied by sessionDatePredicate in src/date-filter.ts. Both from and to are inclusive — a session belongs to the window when the date prefix of its started_at ISO timestamp falls within [from, to] . Filtering now uses direct timestamp comparisons started_at >= ? and started_at < ? where the upper bound is computed as the day after the requested end date (to include the full final day). This approach leverages the started_at index for better query performance while maintaining the same inclusive date-window semantics . The timezone of started_at (UTC) is what matters — not the caller's local time.
Inputs that don't match YYYY-MM-DD or that represent an invalid calendar date are silently discarded by isoDate(). Callers that need a strict contract should validate dates before sending them .
Practical note: Exclude the current UTC date when you want only complete days, since an in-progress day is still open .
Most queries that accept from/to apply the predicate to the session's started_at, so date semantics are consistent across listSessions, searchPage, fileHotspots, mcpUsage, and aggregates .
Tool Call Date Filtering#
listToolCalls is an exception: the call detail rows filter each call by its own timestamp (when the tool was invoked), so a call made just after midnight appears on the day it ran, even if the session started the day before . The summary cards (total calls, error rate, p50/p95 latency, and top tool) use the session-start scope instead, matching GET /api/tools/usage and GET /api/tools/mcp-usage. This means the summary honors tool, server, session, and project filters but ignores errors_only and min_ms, so summary.calls can differ from total .
Session Counts vs. Metric Aggregation#
The sessions field in aggregate statistics counts only top-level (non-subagent) sessions — those where is_subagent = 0 . Message, tool-call, token, and estimated-cost totals include all visible sessions (root + subagent) in the filtered scope .
The same split applies to dimension breakdowns: the byDimension function sums CASE WHEN s.is_subagent = 0 THEN 1 ELSE 0 END for session counts, while token/cost columns sum across every row in the filtered set .
Model dimension: The model dimension (stats --by model, GET /api/stats/by-dimension?dim=model) is handled separately by byModel. Sessions are counted once, under each session's model label (the model that served most of its requests). Tokens and estimated cost are instead split by the model that actually served each request, using the per-request split in the session_model_usage table. A session that switched models therefore contributes tokens and cost to every model it used, and a model can show tokens and cost with zero sessions. Each session's stored cost is divided among its models in proportion to what their requests cost at current rates, so the rows still sum to the archive total. Reasoning tokens are recorded per session and stay with the session's label. Sessions with no per-request split (for example Codex) fall back to the session's label for tokens and cost.
Why this matters: A single top-level session can spawn many separately metered subagent runs. Counting only roots keeps the session count comparable across different parallelism strategies, while including subagents in cost/token totals gives a complete picture of spend .
The GET /api/sessions list endpoint mirrors this by omitting subagents as top-level rows by default; include_subagents=true includes them as list entries, and with_subagents=true attaches nested summaries .
Warning on double-counting: Do not add a parent session's estimated_cost_usd to archive-wide totals: aggregate statistics already include every visible run .
Subagent Cost Summation#
Each session row carries two subagent cost fields:
| Field | Meaning |
|---|---|
subagent_count | Number of direct child subagent sessions |
subagent_estimated_cost_usd | Cost of direct children only |
These are computed by a LEFT JOIN aggregation in sessionSummarySelect, which groups session rows where is_subagent = 1 and parent_session_id IS NOT NULL .
For nested subagent trees, the UI computes the full recursive cost via sessionThreadCost in src/ui/session-summary.ts:
- If the session has loaded
subagents(i.e.,with_subagents=truewas requested), the function recurses into them. - Otherwise, it falls back to
session.estimated_cost_usd + subagent_estimated_cost_usd(direct children only) .
This means the cost shown in summary cards is a thread total (session + all visible descendants), while the raw estimated_cost_usd on each row is the session's own spend only .
Nested summaries are capped at five levels deep — see buildSubagentDetails and the recursive CTE in getSession . For a complete deeper tree, request subagents as rows and join by parent_session_id .
Key Files#
| File | Role |
|---|---|
src/date-filter.ts | sessionDatePredicate — inclusive UTC date SQL fragments |
src/stats.ts | totals, byDimension, toolUsage, mcpUsage — aggregate statistics with root/subagent split |
src/query.ts | listSessions, sessionSummarySelect — per-session summaries and subagent rollups |
src/ui/session-summary.ts | sessionThreadCost, sessionCardMetrics — recursive UI-side cost summation |
docs/analytics-methodology.md | Canonical definitions for all metric semantics |
docs/api/recipes.md | Reproducible curl query patterns with date windows |