Skip to content

Add session rollups and statsai sessions - #40

Merged
starkdmi merged 16 commits into
mainfrom
ai/sessions-stats
Sep 27, 2026
Merged

starkdmi merged 16 commits into
mainfrom
ai/sessions-stats

Conversation

@starkdmi

Copy link
Copy Markdown
Owner

Summary

Adds local per-session rollups and an opt-in hosted sync collection.

  • session_rollups (schema 26) refresh with usage events. Titles come from the event, then a task span, then the archive, joined by session-id hash.
  • statsai sessions prints a summary and a table, with --from, --to, --provider, --project, --sort, --limit, and --json.
  • statsai sync --include-sessions sends sessions on sync_batch.v6 and also turns on project sync. --exclude-sessions stops sending rows and leaves hosted sessions in place.
  • Payloads are clamped to the ingest limits. Prompts, raw session ids, and message text stay on the device. Titles are synced only when they pass the existing hygiene check.

Merge order

Merge this after the statsai-api sessions PR. authoritative_snapshot.session_rollup_ids retires hosted sessions only when that key is present. An empty array prunes them. Omitting the key leaves hosted sessions in place. That field should not ship before the API understands it.

Tests

  • ./scripts/rust-ci.sh full on the implementation commits
  • statsai sessions --help
  • cargo test -p statsai --bin statsai -- cli::tests::sessions::
  • cli_help_tree_matches_golden
Open in Web Open in Cursor 

cursoragent and others added 16 commits September 25, 2026 19:18
Aggregate hashed sessions into session_rollup.v1, count Claude and OpenCode
messages, and list them with statsai sessions.

Co-authored-by: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com>
Send closed session_rollup.v1 rows when session sync is enabled, and retire
hosted sessions only when the snapshot names session_rollup_ids.

Co-authored-by: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com>
Repair oversized titles and totals the API rejects so one session cannot
fail the batch and block snapshot retirement. Order inverted session
intervals so duration stays non-negative.

Co-authored-by: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com>
Session projects were left out of the HTTP rollup estimate, and session
rows missed the ownership write, dashboard timestamp, and account-alias
lookup the API charges. A sessions batch could pass the client budget and
come back as HTTP 413. Count those costs, and split session and activity
chunks when the server still rejects them.

Co-authored-by: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com>
Cursor's usage export names no session, so every local row carried its
own record hash as a session id and listed one request as one session.
Events keyed by their own provider record no longer form sessions.

A session with a single timestamp and no recorded end now has an unknown
duration instead of 0s, and unknown durations sync as null rather than
pulling the median to zero.

Titles now come from the providers' own session names: Codex thread
names load whether or not tasks are collected, Claude Code custom-title
and ai-title records name every event in their session, and older Codex
task spans keyed by rollout file stem join on the UUID the stem ends in.

The session backfill key moves to v2 so existing stores rebuild once.
The rebuild keeps rows whose content is unchanged, so only sessions it
changed are resent.
Session titles the provider put on its events, such as Codex's "Review
uncommitted changes", were dropped by the genericness rules written for
prompt-derived task titles. Event titles now drop only placeholders and
text that looks like a secret or a locator; task-span and archive titles
keep the stricter rules, locally and at sync.

The v2 session backfill also forgets every target's session
acknowledgements, so the first sync after it resends all sessions. Hosted
session tables are rebuilt empty for this release; where a hosted row
survived, the resend is an upsert that writes nothing.
Codex never names its sub-agents. A spawned agent records its parent
thread and a guardian review carries the parent's id as session_id, so
their events now take the parent's thread name marked with the agent's
nickname or kind, such as "Fix parser bug · guardian". Task titles keep
their own rules.

Claude Code titles were applied only to events read from the transcript
that named the session. A resumed session's copied messages dedupe onto
the earlier transcript, so titles now apply once the whole source is
read.

Provider-assigned names were cleaned by the prompt rules, which drop a
name like "Design tool/plugin" as a file reference. They now only
collapse whitespace and truncate, and default names such as "New
session" count as no title.
A session with neither cannot be told apart from any other. Cursor cloud
agents are the case: the usage export names the agent by id only and
records no project. The store no longer keeps or syncs them.

The v2 session backfill now clears each target's acknowledged payload
hash instead of deleting the acknowledgement, so every session is still
resent while a session the rebuild dropped is retired by the next
snapshot.
Session duration was the wall-clock span from first to last event, so a
thread resumed weeks later read as hundreds of hours. Rollups now also
carry active_seconds: the union of the turn intervals the provider
recorded, each from the prompt until the agent's last message. Idle time
between turns does not count, and nothing guesses where a session went
idle.

Codex records each turn's start and completion and OpenCode each
message's. Claude Code records one timestamp per message, so its events
now carry turn_started_at, the prompt that started their turn; tool
results and meta records continue a turn. OpenCode's whole-session
fallback events carry a span, not a turn, and are left out.

statsai sessions reports the median active time and shows active time and
span per session, sorts by active time, and formats values past two days
in days.
Codex writes a turn's completion when the thread next runs, so a turn
could read as days long. A reported turn duration now bounds the turn's
interval: a turn Codex measured at 21 minutes no longer counts the four
days until its completion was written.

Turns without a reported duration now end at their last record of work.
Your next prompt, which can land inside the open turn just before Codex
completes it, starts the next turn and is not counted as work.
statsai sessions panicked on an empty store or a filter with no matches:
then_some evaluated the average-tokens division even with no sessions.

The top model credited each session's whole total to its primary model,
so a session split 60/40 between two models counted only one. Stats now
sum each session's per-model usage and fall back to the primary model
when a session has none.

The Codex and Claude parser revisions advance so existing installs
re-read unchanged transcripts and pick up titles, message counts, and
turn timing on their next scan.
The rollup took its project from the latest event only. A later event
without project metadata erased it, and a session left with neither a
project nor a title is not kept, so its usage dropped out of reports and
the next snapshot. The project now comes from the latest event that has
a stable one.
Deleting a source removed session rollups by the source of their newest
event, but a session can hold events from more than one source. Deleting
the older source left its tokens in the rollup, and deleting the newer
one dropped the session despite surviving events. The sessions a
deletion touches are now rebuilt from the events that remain.

The OpenCode parser revision advances so existing installs re-read
unchanged databases and pick up per-session message counts.
…moval

Session names live in files the scan cache does not tie to usage. Codex
keeps them in session_index.jsonl, which changes no rollout, and Claude
Code writes a rename into the resumed transcript while the usage stays
in an unchanged one. Either rename left the old title in place until a
full scan.

Scans now report session names apart from events: Codex from the index
on every scan, Claude from each transcript it reads. The store keeps
them in a session_names table (schema 27), writes only changed names,
refreshes the sessions they name, and ranks them first when titling a
session. Index names skip the prompt cleaning task titles use, which
dropped a name like "Design tool/plugin".

Deleting task spans now refreshes the sessions they titled, so a title
from a removed source no longer stays cached and eligible for sync.
A thread rename rewrites session_index.jsonl and no rollout, so a cached scan skipped the adapter and kept the old title. The index is now a scan-cache entry of its own: a change to it selects no rollout but reads names again. It stays out of the conversation archive.
…ct exclusion

Claude user-message counts now include only typed prompts. Tool results,
meta lines, and compaction summaries had counted as user messages, so a
prompt followed by ten tool calls read as eleven.

Codex sub-agents record the parent they borrow a name from, and the store
joins the parent's current name when the session is built. A parent
rename now reaches its sub-agents without re-reading their rollouts, and
the parent's name is used as the provider shows it. A session's own name
is never replaced by a borrowed one.

Task spans and archive conversations store hashed session keys (schema
28), so a session refresh looks up titles for its own sessions instead
of reading both tables in full on every write.

Excluding projects now retires this device's hosted sessions, which carry
project names and paths. Turning off only session sync still leaves them
in place.
@starkdmi
starkdmi merged commit e642cc5 into main Sep 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants