Skip to content

feat(usage): show 5h/weekly rate limits on the usage page and sidebar hover - #5739

Open
t3dotgg wants to merge 8 commits into
mainfrom
t3code/usage-limits-analytics
Open

feat(usage): show 5h/weekly rate limits on the usage page and sidebar hover#5739
t3dotgg wants to merge 8 commits into
mainfrom
t3code/usage-limits-analytics

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 8, 2026

Copy link
Copy Markdown
Member

You could never see how much of your Claude or Codex subscription limits you had left without leaving T3 Code. Both adapters were already receiving rate-limit events and throwing them away.

Now the usage page opens with a Limits strip (Claude: 5h, weekly, Fable; Codex: weekly) above the existing analytics, and hovering the sidebar Usage button shows the same availability at a glance with reset times.

How it works:

  • New `AccountLimitsService` caches one normalized snapshot per provider. It ingests the `account.rate-limits.updated` events the adapters already emit, and persists across restarts since Claude limits never hit disk.
  • Codex is additionally seeded from the `rate_limits` object its transcripts write beside every token count, so data shows up even before any session runs through T3 Code.
  • The Claude adapter now pulls the full window set through the SDK usage control request (throttled to once per 3 minutes) because the streamed event only ever names the currently binding window.
  • Windows are data, not schema: Codex's paused 5-hour window auto-reappears when the API ships it again. Spark and the opus/sonnet scoped weeklies are hidden.

Written by Claude Fable 5 via Claude Code.


Note

Medium Risk
Touches runtime ingestion and provider adapters (including an experimental Claude SDK API) plus on-disk cache writes, but exposure is read-scoped RPC and subscription metering data rather than auth or secrets.

Overview
Subscription rate limits (5h / weekly / Fable for Claude, weekly for Codex) are now visible inside T3 Code via a new Limits strip on the usage page and a hover card on the sidebar Usage button.

The server adds AccountLimitsService: it normalizes provider payloads, keeps one snapshot per provider, persists to account-limits.json, and serves server.getAccountLimits (orchestration read scope). ProviderRuntimeIngestion ingests account.rate-limits.updated before thread resolution so snapshots are not dropped when a thread is gone. Codex can be seeded from session transcript tails on read when live data is stale; Claude relies on the live stream and the new throttled (~3 min) SDK usage control pull at session init and on rate_limit_event (in addition to existing single-window events).

The web client useAccountLimits merges the freshest snapshot per provider across environments; usage refresh also refreshes limits.

Reviewed by Cursor Bugbot for commit 54d006d. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Show 5h/weekly rate limits on the usage page and sidebar hover card

  • Adds a new AccountLimitsService that maintains a persisted, server-wide cache of provider rate-limit snapshots in stateDir/account-limits.json, surviving restarts and merging the freshest snapshot per provider.
  • The Claude adapter periodically fetches account usage via the SDK control API (throttled to once per 3 minutes) and emits account.rate-limits.updated runtime events on session init and rate-limit telemetry; ProviderRuntimeIngestion handles these events regardless of thread existence.
  • A new server.getAccountLimits RPC (requiring AuthOrchestrationReadScope) exposes the limits summary to clients via a cached atom with a 60s stale time.
  • Adds an AccountLimitsSection to the Usage page and an AccountLimitsHoverCard shown when hovering the sidebar Usage button, both displaying per-provider meters with percent used and reset countdowns.
  • Codex limits are backfilled from on-disk session transcripts when newer than the cache.

Macroscope summarized 54d006d.

Summary by CodeRabbit

  • New Features
    • Added account rate-limit visibility for Claude and Codex.
    • View usage windows, percentages, reset times, plan details, and data freshness.
    • Added account-limit summaries to the Usage page and Usage sidebar hover card.
    • Supports multiple environments and refreshes account-limit data alongside usage data.
    • Preserves recent limit information when live provider data is temporarily unavailable.
  • Improvements
    • Usage indicators highlight warning and critical thresholds at 80% and 95%.
    • Reset countdowns and snapshot age update automatically.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds account-limit contracts, provider normalization and caching, Claude refresh events, a server RPC, multi-environment client state, and web displays in the usage page and sidebar.

Changes

Account limits

Layer / File(s) Summary
Account-limit contracts and RPC
packages/contracts/src/accountLimits.ts, packages/contracts/src/rpc.ts, packages/contracts/src/index.ts, apps/server/src/auth/RpcAuthorization.ts
Defines account-limit schemas and registers the authorized serverGetAccountLimits RPC.
Normalization and account-limit storage
apps/server/src/usage/accountLimitsNormalize.ts, apps/server/src/usage/accountLimitsNormalize.test.ts, apps/server/src/usage/accountLimitsTranscripts.ts, apps/server/src/usage/AccountLimitsService.ts
Normalizes Claude and Codex data, recovers Codex snapshots from transcripts, and persists fresh provider snapshots.
Provider refresh and runtime ingestion
apps/server/src/provider/Layers/ClaudeAdapter.ts, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts, apps/server/integration/OrchestrationEngineHarness.integration.ts, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts, apps/server/src/server.ts, apps/server/src/server.test.ts
Refreshes Claude usage, emits rate-limit events, ingests them before thread lookup, and wires production and test dependencies.
Server endpoint and client query
apps/server/src/ws.ts, packages/client-runtime/src/state/server.ts
Returns account-limit summaries through WebSocket and caches each environment query for 60 seconds.
Web account-limit presentation
apps/web/src/state/accountLimits.ts, apps/web/src/usage/limitsFormat.ts, apps/web/src/components/usage/AccountLimits.tsx, apps/web/src/components/usage/UsagePage.tsx, apps/web/src/components/sidebar/SidebarChrome.tsx
Combines environment snapshots, formats reset and age values, and renders limits in the usage page and sidebar hover card.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UsagePage
  participant AccountLimitsState
  participant ServerGetAccountLimits
  participant AccountLimitsService
  UsagePage->>AccountLimitsState: refresh account limits
  AccountLimitsState->>ServerGetAccountLimits: query each environment
  ServerGetAccountLimits->>AccountLimitsService: readSummary()
  AccountLimitsService-->>ServerGetAccountLimits: AccountLimitsSummary
  ServerGetAccountLimits-->>AccountLimitsState: environment snapshot
  AccountLimitsState-->>UsagePage: freshest provider snapshots
Loading

Possibly related PRs

  • pingdotgg/t3code#4244: Both changes modify Claude stream-message handling and rate-limit event processing.

Suggested reviewers: juliusmarminge

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: displaying Claude and Codex rate limits in the usage page and sidebar hover card.
Description check ✅ Passed The description clearly explains the changes, rationale, implementation, and UI impact, although it omits the template headings, screenshots, and checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/usage-limits-analytics

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/AccountLimitsService.ts
Comment thread apps/server/src/usage/accountLimitsTranscripts.ts
Comment thread apps/server/src/usage/AccountLimitsService.ts
Comment thread apps/server/src/usage/AccountLimitsService.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 −21 B (−0.2%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB −1 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −20 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 +35 B (+0.3%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB +4 B (+0.1%) 7.3 KiB
Claude Live turn WebSocket wire 5.8 KiB 5.9 KiB +31 B (+0.5%) 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: 886195e · PR result: 54d006d · 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.

@macroscopeapp

macroscopeapp Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR introduces a new feature with significant scope: new AccountLimitsService, new RPC endpoint, new UI components for rate limit display, and modifications to the Claude adapter for event emission. New capabilities of this magnitude warrant human review. An unresolved Medium finding also identifies a potential data correctness issue when multiple Claude instances are configured.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/server/src/usage/AccountLimitsService.ts Outdated
t3dotgg and others added 7 commits August 8, 2026 19:11
Both provider adapters already emitted account.rate-limits.updated events,
but the payload was typed unknown and nothing consumed them. This adds an
AccountLimitsService that caches one normalized snapshot per provider, fed
passively from those events, seeded for Codex from the rate_limits objects
its transcripts already carry, and persisted across restarts (Claude limits
never hit disk). The Claude adapter now also pulls the full window set (5h,
weekly, Fable) through the SDK usage control request, throttled, since the
streamed event only names the binding window.

Exposed via server.getAccountLimits and rendered as a Limits strip on the
usage page plus a hover card on the sidebar Usage button. Windows render
dynamically, so Codex's paused 5-hour window reappears on its own when the
API ships it again. Spark and the opus/sonnet scoped weeklies are hidden.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Concurrent persists (ingestion worker vs the RPC-path transcript seed)
could interleave writes and corrupt account-limits.json, silently dropping
the Claude snapshot on restart. Reuse writeFileStringAtomically. Also raise
the transcript scan cap from 8 to 32 files so a run of Spark-only or
abandoned sessions cannot mask an older main-meter snapshot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Atomic rename prevented torn JSON but not ordering: a slow earlier persist
could land after a newer one and drop a provider snapshot from the file.
The encode now runs inside a one-permit semaphore, so the last writer
always persists the latest map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one-permit lock now covers the ordering guard, the map write, and the
persist for both the event ingest and the transcript seed, instead of only
the file write. Two concurrent mutations could previously both pass their
guard against the same prior snapshot and land in either order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every percentage now reads 'N% used' in both the hover card and the page
strip. Drop plan text, the hover footer, and the freshness caption unless
the snapshot is actually stale (>15m). 24h times, m/d dates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Drop the fixed column widths (nowrap instead) and the redundant countdown
suffix; the reset line is just 'resets 8/15 13:34'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg
t3dotgg force-pushed the t3code/usage-limits-analytics branch from d5ed55b to a382330 Compare August 9, 2026 02:12
Comment thread apps/web/src/components/usage/AccountLimits.tsx
Comment thread apps/web/src/components/usage/AccountLimits.tsx
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts
Comment thread apps/server/src/usage/accountLimitsTranscripts.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 a382330. Configure here.

Comment thread apps/web/src/components/usage/UsagePage.tsx
- Call the SDK usage method through the query object; an extracted
  reference loses its receiver and the catch made that failure silent.
- Treat a backwards wall-clock step as an expired throttle.
- Pick the codex transcript snapshot by its own timestamp, not file mtime.
- Per-provider loading state: a provider without a snapshot shows Loading
  while any environment is still answering, not 'No limit data yet'.
- The usage page refresh button now refreshes the limits cache too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
if (!usage || usage.rate_limits === null || usage.rate_limits === undefined) return;

const stamp = yield* makeEventStamp();
yield* offerRuntimeEvent({

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 Layers/ClaudeAdapter.ts:3457

The account.rate-limits.updated event emitted by emitAccountUsageSnapshot carries only provider: PROVIDER and omits the owning instance's id, but this adapter supports multiple Claude instances on different accounts. ProviderRuntimeIngestion keys account-limit snapshots by provider, so a usage fetch from one configured Claude instance overwrites the limits stored for a different instance's account, and the usage UI can show limits for the wrong account. Consider including providerInstanceId: boundInstanceId (or context.session.providerInstanceId) on this event so AccountLimitsService can keep per-instance snapshots.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/provider/Layers/ClaudeAdapter.ts around line 3457:

The `account.rate-limits.updated` event emitted by `emitAccountUsageSnapshot` carries only `provider: PROVIDER` and omits the owning instance's id, but this adapter supports multiple Claude instances on different accounts. `ProviderRuntimeIngestion` keys account-limit snapshots by provider, so a usage fetch from one configured Claude instance overwrites the limits stored for a different instance's account, and the usage UI can show limits for the wrong account. Consider including `providerInstanceId: boundInstanceId` (or `context.session.providerInstanceId`) on this event so `AccountLimitsService` can keep per-instance snapshots.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts (1)

250-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a focused test for the new rate-limit ingestion branch.

This file only wires AccountLimitsService.layerTest. No test proves that account.rate-limits.updated reaches accountLimits.ingest, or that ingestion still happens when the thread no longer exists, which is the stated reason for placing the call before the thread lookup. Provide a recording stub layer instead of layerTest, emit the event, await drain, then assert the captured input. The existing drain helper avoids arbitrary timeouts.

As per coding guidelines: "Backend behavior changes must include focused tests for that behavior, and tests must not rely on arbitrary timeouts to pass."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts` at
line 250, Add a focused test in the provider runtime ingestion setup by
replacing AccountLimitsService.layerTest with a recording stub layer for
AccountLimitsService. Emit an account.rate-limits.updated event, await the
existing drain helper, and assert that accountLimits.ingest received the
expected input, including the case where the thread no longer exists; do not use
arbitrary timeouts.

Source: Coding guidelines

apps/server/src/usage/AccountLimitsService.ts (1)

49-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the double cast around LimitsCacheFile.

Schema.fromJsonString is used with the schema directly in this codebase, and the cast only hides encoding/decoding type mismatches for the persisted cache.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/usage/AccountLimitsService.ts` around lines 49 - 55, Update
decodeLimitsCache and encodeLimitsCache to pass LimitsCacheFile directly to
Schema.fromJsonString, removing the intermediate unknown and Schema.Codec casts
while preserving the existing cache encoding and decoding behavior.

Source: Coding guidelines

apps/server/src/usage/accountLimitsNormalize.test.ts (2)

95-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert literal ISO strings instead of recomputing them.

Lines 99 and 131 build the expected value with the same DateTime.formatIso(DateTime.makeUnsafe(...)) call the implementation uses. A change in isoFromUnixSeconds would keep these tests green. Hard-coded ISO literals pin the wire format.

♻️ Proposed change
-      resetsAt: DateTime.formatIso(DateTime.makeUnsafe(1_786_600_800_000)),
+      resetsAt: "2026-08-08T23:20:00.000Z",

Replace the literal with the value the current implementation produces, then drop the effect/DateTime import if it becomes unused.

Also applies to: 126-134

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/usage/accountLimitsNormalize.test.ts` around lines 95 - 102,
Update the expected resetsAt values in the account-limits normalization tests to
hard-coded ISO string literals matching the current wire format, rather than
calling DateTime.formatIso and DateTime.makeUnsafe. Apply this to both affected
expectations and remove the effect/DateTime import if it is no longer used.

34-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case where a flat key and a limits entry describe the same window.

accountLimitsNormalize.ts line 149 states that array entries win because the flat keys go null first. This test sets five_hour: null, so the flat parse rejects it before the array parse runs. The precedence rule is never exercised. Add a payload where five_hour carries a live utilization and the array also carries a session entry, then assert the array percent wins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/usage/accountLimitsNormalize.test.ts` around lines 34 - 63,
Extend the test case in “reads the newer limits array, including a Fable-scoped
weekly” so rate_limits.five_hour contains a live value while limits also
includes a session entry for the same window with a different percent. Update
the expected five_hour utilization to the limits-array percent, preserving the
existing assertions for the other windows, so the flat-key versus array-entry
precedence is exercised.
apps/web/src/usage/limitsFormat.ts (1)

7-31: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The en-US locale fixes the date order for every user.

8/12 16:00 reads as 12 August in the US and as 8 December elsewhere. Passing undefined as the locale uses the user's system settings and keeps hourCycle: "h23" for the 24-hour clock.

Line 31 depends on the en-US output shape. .replace(", ", " ") only matches the US separator, so it must go if the locale becomes dynamic. Prefer formatToParts, or accept the locale's own separator.

♻️ Proposed change
-const TIME = new Intl.DateTimeFormat("en-US", {
+const TIME = new Intl.DateTimeFormat(undefined, {
   hourCycle: "h23",
   hour: "2-digit",
   minute: "2-digit",
 });
-const DATE_TIME = new Intl.DateTimeFormat("en-US", {
+const DATE_TIME = new Intl.DateTimeFormat(undefined, {
   month: "numeric",
   day: "numeric",
   hourCycle: "h23",
   hour: "2-digit",
   minute: "2-digit",
 });

Keep en-US if the rest of the usage page already fixes the locale on purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/usage/limitsFormat.ts` around lines 7 - 31, Update the TIME and
DATE_TIME formatters in formatResetAt to use the user’s system locale by passing
undefined instead of hard-coding "en-US", unless the surrounding usage page
intentionally standardizes en-US. Remove the locale-specific `.replace(", ", "
")` transformation and preserve correct locale-specific date separators,
preferably by formatting through formatToParts.
apps/server/src/provider/Layers/ClaudeAdapter.ts (1)

3437-3454: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the SDK usage request with a timeout.

Effect.promise at line 3445 never settles if the SDK control request hangs. The caller forks the effect detached, so nothing observes the hang and nothing interrupts it. One fiber leaks per hung request, and stopSessionInternal does not track it. The adapter already bounds a comparable SDK control call in interruptTurn with Effect.timeoutOption("3 seconds").

Separately, lastAccountUsageFetchAtMs is one counter shared by every session in the adapter closure. Line 3438 yields before line 3443 writes it, so two sessions that initialize in the same scheduler tick can both pass the throttle check and both issue a request. The cost is one extra request, so a fix is optional, but the shared-counter semantics are worth an explicit note in the comment on lines 3427-3433.

♻️ Proposed timeout
     const usage = yield* Effect.promise(async () => {
       try {
         // Called through the query object so the SDK method keeps its
         // receiver; an extracted reference loses `this` and throws.
         return await context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET?.();
       } catch {
         return undefined;
       }
-    });
+    }).pipe(
+      Effect.timeoutOption("15 seconds"),
+      Effect.map((result) => Option.flatMapNullable(result, (value) => value)),
+      Effect.map(Option.getOrUndefined),
+    );

Option is already imported in this file (see interruptTurn). Simplify the pipe if a plain Effect.timeoutTo reads better here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts` around lines 3437 - 3454,
Bound the SDK usage request in the usage-fetch effect around
context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET with the
existing three-second timeout pattern used by interruptTurn, while preserving
the current undefined-on-failure behavior. Also update the comment describing
lastAccountUsageFetchAtMs to explicitly note that it is shared across sessions
and that concurrent initialization can issue one extra request.
apps/web/src/state/accountLimits.ts (1)

63-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider placing the freshest-per-provider selection in packages/client-runtime.

useAccountLimits holds the cross-environment merge rule, the contract-version gate, and the pending/settling semantics. The RPC layer already lives in packages/client-runtime/src/state/server.ts. Mobile and desktop need the same selection rule to show the same limits. Extracting the pure part — freshest snapshot per provider from a list of environment statuses — into packages/client-runtime keeps the web hook as a thin binding.

This is a follow-up, not a blocker for this PR.

As per coding guidelines: "Frontend behavior must support all applicable clients: web, desktop/Electron, and mobile/React Native; shared logic belongs in packages/client-runtime when appropriate."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/state/accountLimits.ts` around lines 63 - 99, Extract the pure
freshest-per-provider selection logic from useAccountLimits into
packages/client-runtime, using a shared function that accepts environment
statuses and returns the newest snapshot for each UsageProviderKind. Update
useAccountLimits to call this shared helper while retaining its hook-specific
refresh, contract-version, pending, and settling behavior; ensure the same
selection rule is available to desktop and mobile clients.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/usage/AccountLimitsService.ts`:
- Around line 231-235: Update maybeSeedCodexFromTranscripts so a backward
wall-clock change resets lastCodexSeedAttemptAtMs when nowMs is earlier than the
recorded attempt, allowing seeding to proceed immediately. Preserve the existing
interval throttle for timestamps that are not earlier.

In `@apps/server/src/usage/accountLimitsTranscripts.ts`:
- Around line 84-89: Update the tail-reading logic around handle.read to capture
its returned bytesRead value, then decode only the buffer range containing
bytesRead before splitting into lines. Preserve the existing partial-first-line
handling and scan behavior.

---

Nitpick comments:
In `@apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts`:
- Line 250: Add a focused test in the provider runtime ingestion setup by
replacing AccountLimitsService.layerTest with a recording stub layer for
AccountLimitsService. Emit an account.rate-limits.updated event, await the
existing drain helper, and assert that accountLimits.ingest received the
expected input, including the case where the thread no longer exists; do not use
arbitrary timeouts.

In `@apps/server/src/provider/Layers/ClaudeAdapter.ts`:
- Around line 3437-3454: Bound the SDK usage request in the usage-fetch effect
around context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET
with the existing three-second timeout pattern used by interruptTurn, while
preserving the current undefined-on-failure behavior. Also update the comment
describing lastAccountUsageFetchAtMs to explicitly note that it is shared across
sessions and that concurrent initialization can issue one extra request.

In `@apps/server/src/usage/accountLimitsNormalize.test.ts`:
- Around line 95-102: Update the expected resetsAt values in the account-limits
normalization tests to hard-coded ISO string literals matching the current wire
format, rather than calling DateTime.formatIso and DateTime.makeUnsafe. Apply
this to both affected expectations and remove the effect/DateTime import if it
is no longer used.
- Around line 34-63: Extend the test case in “reads the newer limits array,
including a Fable-scoped weekly” so rate_limits.five_hour contains a live value
while limits also includes a session entry for the same window with a different
percent. Update the expected five_hour utilization to the limits-array percent,
preserving the existing assertions for the other windows, so the flat-key versus
array-entry precedence is exercised.

In `@apps/server/src/usage/AccountLimitsService.ts`:
- Around line 49-55: Update decodeLimitsCache and encodeLimitsCache to pass
LimitsCacheFile directly to Schema.fromJsonString, removing the intermediate
unknown and Schema.Codec casts while preserving the existing cache encoding and
decoding behavior.

In `@apps/web/src/state/accountLimits.ts`:
- Around line 63-99: Extract the pure freshest-per-provider selection logic from
useAccountLimits into packages/client-runtime, using a shared function that
accepts environment statuses and returns the newest snapshot for each
UsageProviderKind. Update useAccountLimits to call this shared helper while
retaining its hook-specific refresh, contract-version, pending, and settling
behavior; ensure the same selection rule is available to desktop and mobile
clients.

In `@apps/web/src/usage/limitsFormat.ts`:
- Around line 7-31: Update the TIME and DATE_TIME formatters in formatResetAt to
use the user’s system locale by passing undefined instead of hard-coding
"en-US", unless the surrounding usage page intentionally standardizes en-US.
Remove the locale-specific `.replace(", ", " ")` transformation and preserve
correct locale-specific date separators, preferably by formatting through
formatToParts.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0835995-9fd0-4c4e-b754-3607a77b46e3

📥 Commits

Reviewing files that changed from the base of the PR and between 886195e and 54d006d.

📒 Files selected for processing (21)
  • apps/server/integration/OrchestrationEngineHarness.integration.ts
  • apps/server/src/auth/RpcAuthorization.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
  • apps/server/src/provider/Layers/ClaudeAdapter.ts
  • apps/server/src/server.test.ts
  • apps/server/src/server.ts
  • apps/server/src/usage/AccountLimitsService.ts
  • apps/server/src/usage/accountLimitsNormalize.test.ts
  • apps/server/src/usage/accountLimitsNormalize.ts
  • apps/server/src/usage/accountLimitsTranscripts.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/sidebar/SidebarChrome.tsx
  • apps/web/src/components/usage/AccountLimits.tsx
  • apps/web/src/components/usage/UsagePage.tsx
  • apps/web/src/state/accountLimits.ts
  • apps/web/src/usage/limitsFormat.ts
  • packages/client-runtime/src/state/server.ts
  • packages/contracts/src/accountLimits.ts
  • packages/contracts/src/index.ts
  • packages/contracts/src/rpc.ts

Comment on lines +231 to +235
const maybeSeedCodexFromTranscripts = Effect.fn("AccountLimitsService.seedCodex")(function* (
nowMs: number,
) {
if (nowMs - lastCodexSeedAttemptAtMs < CODEX_SEED_MIN_INTERVAL_MS) return;
lastCodexSeedAttemptAtMs = nowMs;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a backward clock change in the seed throttle.

Clock.currentTimeMillis follows the wall clock. If the host clock moves backwards, nowMs - lastCodexSeedAttemptAtMs becomes negative, so the guard is true and the transcript seed is skipped until the clock passes the old timestamp. The PR objectives state that throttle handling covers backward clock changes; this guard does not. Reset the marker when nowMs is earlier than the last attempt.

🐛 Proposed fix
-    if (nowMs - lastCodexSeedAttemptAtMs < CODEX_SEED_MIN_INTERVAL_MS) return;
+    const elapsedMs = nowMs - lastCodexSeedAttemptAtMs;
+    // A backward host clock must not park the seed until the clock catches up.
+    if (elapsedMs >= 0 && elapsedMs < CODEX_SEED_MIN_INTERVAL_MS) return;
     lastCodexSeedAttemptAtMs = nowMs;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const maybeSeedCodexFromTranscripts = Effect.fn("AccountLimitsService.seedCodex")(function* (
nowMs: number,
) {
if (nowMs - lastCodexSeedAttemptAtMs < CODEX_SEED_MIN_INTERVAL_MS) return;
lastCodexSeedAttemptAtMs = nowMs;
const maybeSeedCodexFromTranscripts = Effect.fn("AccountLimitsService.seedCodex")(function* (
nowMs: number,
) {
const elapsedMs = nowMs - lastCodexSeedAttemptAtMs;
// A backward host clock must not park the seed until the clock catches up.
if (elapsedMs >= 0 && elapsedMs < CODEX_SEED_MIN_INTERVAL_MS) return;
lastCodexSeedAttemptAtMs = nowMs;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/usage/AccountLimitsService.ts` around lines 231 - 235, Update
maybeSeedCodexFromTranscripts so a backward wall-clock change resets
lastCodexSeedAttemptAtMs when nowMs is earlier than the recorded attempt,
allowing seeding to proceed immediately. Preserve the existing interval throttle
for timestamps that are not earlier.

Comment on lines +84 to +89
const buffer = Buffer.alloc(length);
await handle.read(buffer, 0, length, start);

// The first line may be cut mid-record by the tail offset; JSON.parse
// rejects it and the scan moves on.
const lines = buffer.toString("utf8").split("\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the returned bytesRead when decoding the tail.

handle.read can return fewer bytes than requested. The unread remainder of buffer stays zero-filled, so the decoded string ends with NUL padding and the newest lines can be corrupted or lost. The scan then falls back to older files or returns null. Slice the buffer to bytesRead before decoding.

🐛 Proposed fix
     const buffer = Buffer.alloc(length);
-    await handle.read(buffer, 0, length, start);
+    const { bytesRead } = await handle.read(buffer, 0, length, start);
+    if (bytesRead <= 0) return null;
 
     // The first line may be cut mid-record by the tail offset; JSON.parse
     // rejects it and the scan moves on.
-    const lines = buffer.toString("utf8").split("\n");
+    const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const buffer = Buffer.alloc(length);
await handle.read(buffer, 0, length, start);
// The first line may be cut mid-record by the tail offset; JSON.parse
// rejects it and the scan moves on.
const lines = buffer.toString("utf8").split("\n");
const buffer = Buffer.alloc(length);
const { bytesRead } = await handle.read(buffer, 0, length, start);
if (bytesRead <= 0) return null;
// The first line may be cut mid-record by the tail offset; JSON.parse
// rejects it and the scan moves on.
const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/usage/accountLimitsTranscripts.ts` around lines 84 - 89,
Update the tail-reading logic around handle.read to capture its returned
bytesRead value, then decode only the buffer range containing bytesRead before
splitting into lines. Preserve the existing partial-first-line handling and scan
behavior.

@syrok0010

syrok0010 commented Aug 9, 2026

Copy link
Copy Markdown

Thanks for working on this—usage limits are essential when managing multiple accounts or Plus-tier subs. I’ve been using a custom main + #1732 build for quite a while.

Some feedback on current state of the PR from limited testing: with three Codex instances sharing one homePath but using separate shadowHomePath values, only one account’s limits are shown reliably. Transcript discovery can also assign identical usage to different instances.

#1732 has useful groundwork for per-instance limits. Querying account/rateLimits/read during each instance’s health check gave me the correct account name and usage for all three, without starting a session first.

@eddedre

eddedre commented Aug 9, 2026

Copy link
Copy Markdown

This is awesome! I was just mentioning this on a twitter post. So glad to see this PR 🎉🎉🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚧 In Progress 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.

3 participants