Statistics in the admin console¶
Status: draft M0. Rules: 17-observability.md 8. Presentation in
the console: 01-product.md; visualizations, own measurement and live stream:
25-visualization.md. Permissions: stats.read and cost.read.aggregate
(05-actions.md 4.2, 4.12).
1. Principle¶
The statistics read their own tables in PostgreSQL, never a telemetry backend. Since the
addendum of 2026-10-10 this also includes stats_minutes, the minute aggregates of the own
measurement (section 7). Reason: The
observability stack is optional, sampled and has short retention periods; the statistics must be
the same and complete in every installation and follow the same permissions as the data.
2. Key figures¶
| Area | Key figure | Source | Grouping |
|---|---|---|---|
| Usage | Messages per day by origin (human, agent, app, import) |
messages (without deleted ones) |
Space, channel, day |
| Usage | New threads, done threads | threads |
Space, channel, day |
| Usage | Active people (at least one message) | messages.author_principal_id |
Space, week; only from privacy.stats_min_group_size |
| Usage | Storage of the files | files.size_bytes via scope_channel_id |
Space |
| Runs | Runs by end state, duration (median, p95) | runs |
Agent, space, day |
| Runs | Failure reasons | runs.state_reason for failed |
Agent, day |
| Tools | Tool calls by name, share of failed |
run_steps with kind = 'tool_call' |
Agent, tool, day |
| Costs | Valid sum, share of estimates, by model | cost_entries without a successor |
Space, agent, model, day, currency |
| Error rates | Runs failed per runs |
runs |
Agent, day |
| Error rates | Webhook deliveries failed and dead |
webhook_deliveries (while within the retention period) |
App, day |
| Error rates | Scans error and infected |
files.scan_status |
Day |
| Error rates | Push registrations disabled | push_registrations.disabled_reason |
Provider |
Costs in the mode personal are information only, never an invoice
(03-data-model.md 4.9); the console labels the two modes separately
and estimates as "Estimate".
Source ports (M6-05): every row is read through the StatsSource port of the module that owns
its table, never by stats itself, and only for the channels the caller may read:
| Rows | Port | Note |
|---|---|---|
| Messages by origin, active people, refused triggers | messaging.StatsSource |
active people only per week or month, counted inside messaging |
| New and done threads, channel to space, thread to channel | chat.StatsSource |
done is a thread closed in the bucket and still closed |
| Runs, failure reasons, durations, tool calls | runs.StatsSource |
over the threads of the visible channels (runs has no channel_id) |
| Costs | billing.StatsSource |
valid entries; personal only with billing.show_personal_estimates |
| Storage, scans | files.StatsSource |
scans only for an owner on the instance |
| Webhook deliveries | apps.StatsSource |
only for an owner on the instance |
| Push registrations | notify.StatsSource |
only for an owner on the instance |
Limits of the sources: from run.max_steps_persisted on, run_steps merges older steps; very
long runs therefore count too few tool calls. webhook_deliveries is in the class transient
and reaches back only as far as its retention period. Deleted messages do not count.
3. Privacy¶
- No key figure per person. Neither messages nor runs nor costs are grouped by person. The
triggering person of a run (
triggered_by) does not appear in the statistics. - The console shows counters over people (for example active people per space) below
privacy.stats_min_group_sizeas "fewer thanprivacy.stats_min_group_size". Reason: small groups make individuals recognizable. - The cost view per thread is not statistics but part of the thread (
cost.read.thread). - An export of the statistics as CSV contains only the same aggregates.
4. Permissions¶
| View | Action | Who |
|---|---|---|
| Whole instance | stats.read on instance |
Owner |
| One space | stats.read on space |
manage on the space |
| Total costs | cost.read.aggregate on instance |
Owner |
| Costs of a space | cost.read.aggregate on space |
manage on the space |
Every query filters to the channels that authz returns for the principal; a space manager
sees no figures from channels they may not read.
5. Queries and performance¶
- Queries use the existing indexes (
runs (agent_id, finished_at),cost_entries (space_id, occurred_at),messages (channel_id, created_at)). - Results are valid for
stats.cache_ttl; the period is at moststats.max_range. - Daily aggregates: If a query becomes too expensive (load test), a nightly job writes rows into
a table
stats_daily(space, channel, agent, day, key figure, value) and the console reads from there. The table is not yet in the core contract; it comes as an additive migration only when the load test requires it (integration decision of 2026-10-10). - The time zone of the day boundary is
stats.time_zone(defaultUTC, IANA name; 17-observability.md).locale.defaultdoes not set it. A change applies from the restart on; daily aggregates already written keep their old boundary. - The cache (M6-05) lives in memory per api process. Its key is the operation, the aligned
parameters, the caller's rights version, whether the scope is instance-wide and a hash of the
caller's channel set; an entry expires after
stats.cache_ttl, and at moststats.cache_max_entriesentries are kept (the least recently used one is evicted). Authorization runs before every cache read, so a revoked permission takes effect at once.
6. Relation to telemetry¶
For operational questions (latency, 5xx, connections) the console links to telemetry.dashboard_url,
if set. The statistics themselves never compute with telemetry values.
7. Own measurement and visualizations¶
- The server itself counts values without a domain table (delivery latency, reconnects,
acceptance by the push provider, availability of the API) and the rates of the live views, and
writes them per minute to
stats_minutes(25-visualization.md, section 3). The table has no column for people; tool names are not in it. - New queries:
getStatsLive(live frame),getStatsTimeline(minutes and runs of a period),getStatsSlo(traffic light "Own measurement", owner only),getStatsBackends(backend view per agent). All withstats.readand the same permission filter as section 4; cost fields only withcost.read.aggregate. In addition, the gateway subscriptionstats.subscribewith the ephemeral eventstats.live. Built:getStatsLiveand the subscription (M6-15);getStatsTimeline,getStatsSloandgetStatsBackends(M6-14, rulingsD-M6-14-1toD-M6-14-7). The backend view counts the switches of the backend policy, which have no channel, for the agents it shows (D-M6-14-3). - Rows without a channel (SLI, gateway values, changes of the backend policy) are visible only
to whoever has
stats.readon the instance. - The minimum group from section 3 applies only to counters over people; the live frame, the
timeline and
stats_minutescontain none. Active people are still shown only bygetUsageStats. - Long periods, exports and the tables from section 2 are still computed from the domain tables;
stats_minuteslives only forretention.stats_minutes.