Skip to content

feat(usage): usage page reading provider transcripts across environments - #5684

Merged
t3dotgg merged 12 commits into
mainfrom
t3code/64ba797b
Aug 8, 2026
Merged

feat(usage): usage page reading provider transcripts across environments#5684
t3dotgg merged 12 commits into
mainfrom
t3code/64ba797b

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 8, 2026

Copy link
Copy Markdown
Member

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:
image

Approach

Read the provider CLIs' own on-disk session transcripts, which is the technique ccusage uses:

  • Claude Code: ~/.claude/projects/**/*.jsonlmessage.usage
  • Codex: ~/.codex/sessions/**/*.jsonlevent_msg/token_count

Tokens 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 usage object. Summing naively overcounts by ~2.4x. On a real 30-day window this drops 67,377 duplicate records out of ~132k. Deduped by message.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_usage deltas were checked to reconcile with the session's final total_token_usage before being trusted.

Why WIP

  • Runtime and turn-completion are not implemented. The reference mock shows "221h · 2,092 turns · 98.5% complete". Those cannot come from the transcripts; they need a join against ProjectionTurns. The metric slot currently shows responses and sessions, which are real from this data source. This is the main gap.
  • Cost is API-equivalent, not money spent. Subscription plans bill separately, so the page says "raw token cost" rather than "spend".
  • Codex token_count carries no model, so it is attributed from the preceding turn_context. Sessions that switch models mid-run smear across the switch.
  • Priced at LiteLLM's base tier. The transcripts don't record which pricing tier served a request.
  • Not visually reviewed in a browser yet.

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 UsageService walks 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. serverGetUsageSummary is wired through WS RPC with orchestration read auth and a typed @t3tools/contracts usage contract.

The web app adds /usage (sidebar entry) with cost/token charts, breakdowns, and coverage notices. useUsage queries every connected environment; mergeUsage sums 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

  • Introduces a /usage route rendering an interactive dashboard with time window controls, a stacked provider chart, summary metrics, model/day breakdown tables, and cost quality indicators.
  • Adds a server-side UsageService that 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 a UsageSummary via a new server.getUsageSummary WebSocket RPC.
  • A durable per-file scan cache avoids reparsing unchanged files across requests; cache entries are keyed by file size, mtime, and provider.
  • mergeUsage on the client de-duplicates shared transcript directories across environments (identified by filesystem volumeId), excludes stale environments by contract version, and aggregates cost/token breakdowns by provider, model, and day.
  • Adds a 'Usage' entry to the sidebar footer that navigates to /usage.
  • Risk: transcript parsing is best-effort — read failures return null and are not cached, so a consistently unreadable file is re-attempted on every scan.

Macroscope summarized 721f812.

t3dotgg and others added 3 commits August 7, 2026 23:23
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>
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 87e9e0e5-db7c-4934-b0d8-05a286870f85

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 8, 2026
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/web/src/state/usage.ts Outdated
merged,
environments,
isPending: answeredCount === 0 && environments.some((environment) => environment.isPending),
isPartial: answeredCount > 0 && answeredCount < environments.length,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

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.

🚀 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/server/src/usage/usageTranscripts.ts Outdated
Comment thread packages/contracts/src/usage.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB −24 B (−0.2%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB −8 B (−0.1%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −16 B (−0.3%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB +2 B (+0.0%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB +9 B (+0.2%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB −7 B (−0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 2c7267a · PR result: 721f812 · Source CI: success

Scenario and decoded snapshot size

10 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.

  • Codex decoded thread snapshot: 94.6 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment thread apps/server/src/usage/usageTranscripts.ts Outdated
Comment thread apps/web/src/state/usage.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/web/src/components/usage/UsagePage.tsx
Comment thread apps/web/src/state/usage.ts Outdated
Comment thread apps/web/src/usage/usageFormat.ts
@macroscopeapp

macroscopeapp Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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>
Comment thread apps/web/src/components/usage/UsagePage.tsx
Comment thread apps/web/src/components/usage/UsageProviderChart.tsx Outdated
Comment thread apps/web/src/components/usage/UsageProviderChart.tsx Outdated
Comment thread apps/web/src/components/usage/UsageProviderChart.tsx
t3dotgg and others added 2 commits August 7, 2026 23:58
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>
Comment thread apps/server/src/usage/usageScanCache.ts Outdated
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/server/src/usage/UsageService.ts
Comment thread apps/server/src/usage/UsageService.ts Outdated
t3dotgg and others added 2 commits August 8, 2026 01:10
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>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/contracts/src/usage.ts
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/usageScanCache.ts
Comment thread apps/server/src/usage/UsageService.ts
…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>
@t3dotgg t3dotgg changed the title WIP: feat(usage): usage page reading provider transcripts across environments feat(usage): usage page reading provider transcripts across environments Aug 8, 2026
Comment thread apps/server/src/usage/UsageService.ts Outdated
Comment thread apps/server/src/usage/usageScanCache.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread apps/server/src/usage/UsageService.ts Outdated
…squashed cause

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
t3dotgg and others added 2 commits August 8, 2026 03:50
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotgg merged commit 8101cd0 into main Aug 8, 2026
17 checks passed
@t3dotgg
t3dotgg deleted the t3code/64ba797b branch August 8, 2026 10:59
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 8, 2026
## 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
cursor Bot pushed a commit to aaditagrawal/t3code that referenced this pull request Aug 8, 2026
…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>
omegent-app Bot added a commit to patroza/t3code that referenced this pull request Aug 8, 2026
Adopts the three upstream commits after #381: a cross-environment usage page
reading provider transcripts (pingdotgg#5684), its chart fix (pingdotgg#5697), and one mobile
sheet for model and thread settings (pingdotgg#5625).

Resolutions:

- server.ts / ws.ts / client-runtime state: upstream's UsageService and its
  usageSummary atom family are additive next to the fork's diagnostics services
  (HostResourceProbe, ProcessResourceMonitor, TraceDiagnostics, BackgroundPolicy)
  and hostResourceSnapshot family — unioned.
- ThreadComposer: pingdotgg#5625 folds the model picker and provider options into a
  single settings sheet, replacing the fork's ControlPillMenu. The sheet is
  adopted, and the fork's usage signal rides on it: the trigger keeps
  ProviderUsageIcon with the live marker rather than upstream's plain
  ProviderIcon, so quota state stays visible at a glance. The fork-only
  collapsed-composer pill (upstream has none) now opens the same sheet instead
  of the retired menu.

Adversarial review caught that retiring the model menu also orphaned the fork's
numeric usage note: the marker survived on the trigger icon but the quota
percentage the menu rows carried had no home. It now hangs off the trigger
label, so both halves of the fork's usage signal survive the consolidation.
The plain ProviderIcon import went with upstream's replaced icon.

Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
gmackie pushed a commit to gmackie/t3code that referenced this pull request Aug 8, 2026
…nts (pingdotgg#5684)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant