feat(usage): usage page reading provider transcripts across environments - #5684
Conversation
Adds a usage contract and a server-side scanner that reads the Claude Code and Codex CLIs' own session transcripts rather than T3 Code's orchestration projections, so usage covers turns driven outside T3 Code. Tokens are priced against LiteLLM's rate table, the same source ccusage uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fans the usage query out across every connected environment and merges the results client-side, de-duplicating environments that resolve the same provider transcript directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| merged, | ||
| environments, | ||
| isPending: answeredCount === 0 && environments.some((environment) => environment.isPending), | ||
| isPartial: answeredCount > 0 && answeredCount < environments.length, |
There was a problem hiding this comment.
🟡 Medium state/usage.ts:104
isPartial can be false even when one or more connected environments are excluded from the merged totals. mergeUsage drops summaries whose contractVersion is stale, but useUsage computes isPartial solely from environment.summary !== null. A stale environment still has a non-null summary, so it counts as answered — answeredCount === environments.length, isPartial is false, and the page presents incomplete totals as complete with no coverage warning. Consider including merged.staleEnvironments in the isPartial computation so stale environments are surfaced as missing coverage.
Also found in 1 other location(s)
apps/web/src/components/usage/UsagePage.tsx:85
UsagePagedoes not pass or displaymerged.staleEnvironmentsinUsageCoverageNotice.mergeUsageexplicitly excludes summaries whose contract version is stale, but those environments still count as answered inuseUsage, soisPartialcan be false. If one or more connected servers use an older contract, the page silently presents incomplete (or zero) totals as complete.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/state/usage.ts around line 104:
`isPartial` can be `false` even when one or more connected environments are excluded from the merged totals. `mergeUsage` drops summaries whose `contractVersion` is stale, but `useUsage` computes `isPartial` solely from `environment.summary !== null`. A stale environment still has a non-null summary, so it counts as answered — `answeredCount === environments.length`, `isPartial` is false, and the page presents incomplete totals as complete with no coverage warning. Consider including `merged.staleEnvironments` in the `isPartial` computation so stale environments are surfaced as missing coverage.
Also found in 1 other location(s):
- apps/web/src/components/usage/UsagePage.tsx:85 -- `UsagePage` does not pass or display `merged.staleEnvironments` in `UsageCoverageNotice`. `mergeUsage` explicitly excludes summaries whose contract version is stale, but those environments still count as answered in `useUsage`, so `isPartial` can be false. If one or more connected servers use an older contract, the page silently presents incomplete (or zero) totals as complete.
There was a problem hiding this comment.
Note
🤖 Fable 5 responding on behalf of Theo
Fixed in 4633bce. Stale-version environments are now surfaced in the coverage notice ("runs an older server version and is excluded from totals"), so excluded coverage is visible even when isPartial is false.
There was a problem hiding this comment.
Reviewed the new usage service against the Effect service conventions. Two findings in apps/server/src/usage/UsageService.ts; the contract (packages/contracts/src/usage.ts), the pure helpers, and the web-side atoms/merge code look consistent with the conventions (subpath namespace imports, inline Context.Service interface, dependencies acquired via yield*, Schema.TaggedErrorClass with a message derived from structural attributes).
Posted via Macroscope — Effect Service Conventions
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Needs human review 1 blocking correctness issue found. This PR adds a substantial new feature (usage tracking page) with ~2500+ lines of new code, new RPC endpoints, new services, new UI components, and modifications to auth files. New features of this scope introducing new user-facing behavior warrant human review. You can customize Macroscope's approvability policy. Learn more. |
Leads with cost, adds a real gridline-aligned y-axis and a hover readout, smooths the daily series with monotone cubic interpolation, and defaults the breakdown table to cost by model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tick builder stopped at the last step below the peak, so the scale maximum sat under the highest value and the top of the series was cut off. Round the maximum up to the next step and reserve a sliver above the top gridline for the stroke. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parsed records are written to the state dir keyed by file size and mtime, so a restart reloads them instead of re-parsing the window. Measured on a 30-day scan: 3,542ms cold against 162ms warm, from a 5.4MB cache. Caching per file rather than per day keeps it timezone independent and keeps de-duplication exact. 99% of duplicate records live inside a single file, so entries are stored de-duplicated within their file and the aggregator still runs the global pass that catches the remaining cross-file duplicates. Also replaces the record-cap eviction, which cleared the whole cache once exceeded so a large window never warmed up, with age-based retention that does not evict entries a narrower window simply did not look for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Orders the chart toggle cost-first and makes the left summary follow the active metric, including provider ordering. Replaces colour dots with the existing provider brand marks, whose fills already match the chart bands. Routes the hover tooltip through the same derived series as the chart paths so the readout is always the plotted value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Server: settings failures now surface as UsageReadError instead of reading as zero usage; a Codex token_count arriving before its turn_context no longer poisons the duplicate signature; failed file reads are no longer memoised as empty; the scan-cache load is raced-safe via Effect.cached; the dirty flag only clears after a successful persist; pruning is scoped to walked roots so a missing provider directory cannot purge warm entries; decode validates numeric fields; stale rate tables stop reporting fresh. Contract: UsageDay validates its YYYY-MM-DD shape, sources report distinct session counts, version bumped to 3. Web: session totals come from per-directory distinct counts instead of per-bucket sums; the refresh button refreshes each environment query rather than the derived atom; partial coverage distinguishes still-reporting from failed and surfaces stale-version environments; the window start uses calendar arithmetic so DST cannot shift it; the daily average is labelled per active day; adjacent chart bands share one smoothed curve per stack boundary so edges cannot gap or overlap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
One convention issue found in the new usage service: UsageReadError has no cause and the wrap site stringifies the failure cause into its detail field (which is also what message is built from, and is serialized to clients over RPC).
Posted via Macroscope — Effect Service Conventions
…o window UsageReadError gains an optional structured cause so the failure chain survives without stringified defect text leaking into the wire-serialized message. A corrupt scan-cache row now disqualifies its whole file entry, so the file cold re-parses instead of a partial warm hit silently dropping the corrupt rows' usage. Distinct session counts only include records that actually landed in the requested window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit af0f0ac. Configure here.
…squashed cause Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What's Changed * fix(desktop): zoom shortcuts no longer die when the preview browser has focus by @t3dotgg in pingdotgg/t3code#5691 * feat(mobile): one sheet for model and thread settings by @t3dotgg in pingdotgg/t3code#5625 * feat(usage): usage page reading provider transcripts across environments by @t3dotgg in pingdotgg/t3code#5684 **Full Changelog**: pingdotgg/t3code@v0.0.33-nightly.20260808.1031...v0.0.33-nightly.20260808.1033 Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.33-nightly.20260808.1033
…224) * fix(desktop): zoom shortcuts no longer die when the preview browser has focus (pingdotgg#5691) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(mobile): one sheet for model and thread settings (pingdotgg#5625) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(usage): usage page reading provider transcripts across environments (pingdotgg#5684) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(desktop): zoom shortcuts no longer die when the preview browser has focus (pingdotgg#5691) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(mobile): one sheet for model and thread settings (pingdotgg#5625) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(usage): usage page reading provider transcripts across environments (pingdotgg#5684) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Theo Browne <me@t3.gg> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: aaditagrawal <aaditagrawal@users.noreply.github.com>

Problem
There is no way to see token usage or what the agents are costing. T3 Code's own orchestration projections don't record token counts at all, and even if they did they would miss every turn driven outside T3 Code.
Preview:

Approach
Read the provider CLIs' own on-disk session transcripts, which is the technique
ccusageuses:~/.claude/projects/**/*.jsonl→message.usage~/.codex/sessions/**/*.jsonl→event_msg/token_countTokens are priced against LiteLLM's
model_prices_and_context_window.json, the same rate table ccusage uses, cached to disk with a 24h TTL so the page works offline. Models with no rate are counted in token totals and reported as unpriced rather than silently costed at zero.Each environment returns pre-aggregated
(day, provider, model)buckets; raw transcripts never cross the wire. The client queries every connected environment and merges.Two things worth reviewing closely
De-duplication is load-bearing. T3 Code writes one JSONL record per assistant content block, each repeating the parent message's complete
usageobject. Summing naively overcounts by ~2.4x. On a real 30-day window this drops 67,377 duplicate records out of ~132k. Deduped bymessage.id+requestId, keeping the first, matching ccusage.Cross-environment double counting. Several environments on one machine (worktree servers) resolve the same provider home. Each source reports a fingerprint of
hostId+ resolved path, and the client lets one environment claim each fingerprint, per provider, in a stable order. Without this every worktree server would multiply your totals.Verified against real data
A 30-day scan on this machine: 1,614 files, 3.6s cold, 127 buckets, 917 of 65,124 responses unpriced (1.4%).
Codex
last_token_usagedeltas were checked to reconcile with the session's finaltotal_token_usagebefore being trusted.Why WIP
ProjectionTurns. The metric slot currently shows responses and sessions, which are real from this data source. This is the main gap.token_countcarries no model, so it is attributed from the precedingturn_context. Sessions that switch models mid-run smear across the switch.Reference
Implements direction B, "Cost First", from https://z5mxym83xw02.postplan.dev/
Built with Claude Opus 5 (1M context) in T3 Code.
Note
Medium Risk
New filesystem scans and outbound fetch for pricing on authenticated read RPC; aggregation/dedup logic affects reported totals, with possible slow first loads on large transcript trees.
Overview
Adds a Usage experience that estimates token usage and API-equivalent cost by scanning Claude Code and Codex on-disk session JSONL transcripts (not T3 orchestration projections), priced with a LiteLLM rate table cached on disk.
On the server, a new
UsageServicewalks configured transcript dirs, streams/parses lines with provider-specific reducers, dedupes repeated assistant usage (e.g. per content block), aggregates into(day, provider, model)buckets in the caller’s time zone, and memoizes per-file parses in a durable scan cache keyed by size/mtime.serverGetUsageSummaryis wired through WS RPC with orchestration read auth and a typed@t3tools/contractsusage contract.The web app adds
/usage(sidebar entry) with cost/token charts, breakdowns, and coverage notices.useUsagequeries every connected environment;mergeUsagesums distinct transcript sources and collapses duplicates when multiple environments share the same host/path/volume fingerprint, excluding stale contract versions.Reviewed by Cursor Bugbot for commit 721f812. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add a Usage page that aggregates token and cost data across provider transcript directories
/usageroute rendering an interactive dashboard with time window controls, a stacked provider chart, summary metrics, model/day breakdown tables, and cost quality indicators.UsageServicethat scans Claude and Codex transcript files (.jsonl), parses them line-by-line, prices usage via a LiteLLM rate table (cached with a daily TTL), and returns aUsageSummaryvia a newserver.getUsageSummaryWebSocket RPC.mergeUsageon the client de-duplicates shared transcript directories across environments (identified by filesystemvolumeId), excludes stale environments by contract version, and aggregates cost/token breakdowns by provider, model, and day./usage.nulland are not cached, so a consistently unreadable file is re-attempted on every scan.Macroscope summarized 721f812.