Add session rollups and statsai sessions - #40
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 sessionsprints a summary and a table, with--from,--to,--provider,--project,--sort,--limit, and--json.statsai sync --include-sessionssendssessionsonsync_batch.v6and also turns on project sync.--exclude-sessionsstops sending rows and leaves hosted sessions in place.Merge order
Merge this after the statsai-api sessions PR.
authoritative_snapshot.session_rollup_idsretires 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 fullon the implementation commitsstatsai sessions --helpcargo test -p statsai --bin statsai -- cli::tests::sessions::cli_help_tree_matches_golden