feat(usage): show 5h/weekly rate limits on the usage page and sidebar hover - #5739
feat(usage): show 5h/weekly rate limits on the usage page and sidebar hover#5739t3dotgg wants to merge 8 commits into
Conversation
📝 WalkthroughWalkthroughThe 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. ChangesAccount limits
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
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 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. |
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>
d5ed55b to
a382330
Compare
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 a382330. Configure here.
- 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({ |
There was a problem hiding this comment.
🟡 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts (1)
250-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a focused test for the new rate-limit ingestion branch.
This file only wires
AccountLimitsService.layerTest. No test proves thataccount.rate-limits.updatedreachesaccountLimits.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 oflayerTest, emit the event, awaitdrain, then assert the captured input. The existingdrainhelper 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 valueRemove the double cast around
LimitsCacheFile.
Schema.fromJsonStringis 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 winAssert 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 inisoFromUnixSecondswould 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/DateTimeimport 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 winAdd a case where a flat key and a
limitsentry describe the same window.
accountLimitsNormalize.tsline 149 states that array entries win because the flat keys go null first. This test setsfive_hour: null, so the flat parse rejects it before the array parse runs. The precedence rule is never exercised. Add a payload wherefive_hourcarries a live utilization and the array also carries asessionentry, 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 winThe
en-USlocale fixes the date order for every user.
8/12 16:00reads as 12 August in the US and as 8 December elsewhere. Passingundefinedas the locale uses the user's system settings and keepshourCycle: "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. PreferformatToParts, 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-USif 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 winBound the SDK usage request with a timeout.
Effect.promiseat 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, andstopSessionInternaldoes not track it. The adapter already bounds a comparable SDK control call ininterruptTurnwithEffect.timeoutOption("3 seconds").Separately,
lastAccountUsageFetchAtMsis 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), + );
Optionis already imported in this file (seeinterruptTurn). Simplify the pipe if a plainEffect.timeoutToreads 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 liftConsider placing the freshest-per-provider selection in
packages/client-runtime.
useAccountLimitsholds the cross-environment merge rule, the contract-version gate, and the pending/settling semantics. The RPC layer already lives inpackages/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 — intopackages/client-runtimekeeps 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-runtimewhen 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
📒 Files selected for processing (21)
apps/server/integration/OrchestrationEngineHarness.integration.tsapps/server/src/auth/RpcAuthorization.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.tsapps/server/src/orchestration/Layers/ProviderRuntimeIngestion.tsapps/server/src/provider/Layers/ClaudeAdapter.tsapps/server/src/server.test.tsapps/server/src/server.tsapps/server/src/usage/AccountLimitsService.tsapps/server/src/usage/accountLimitsNormalize.test.tsapps/server/src/usage/accountLimitsNormalize.tsapps/server/src/usage/accountLimitsTranscripts.tsapps/server/src/ws.tsapps/web/src/components/sidebar/SidebarChrome.tsxapps/web/src/components/usage/AccountLimits.tsxapps/web/src/components/usage/UsagePage.tsxapps/web/src/state/accountLimits.tsapps/web/src/usage/limitsFormat.tspackages/client-runtime/src/state/server.tspackages/contracts/src/accountLimits.tspackages/contracts/src/index.tspackages/contracts/src/rpc.ts
| const maybeSeedCodexFromTranscripts = Effect.fn("AccountLimitsService.seedCodex")(function* ( | ||
| nowMs: number, | ||
| ) { | ||
| if (nowMs - lastCodexSeedAttemptAtMs < CODEX_SEED_MIN_INTERVAL_MS) return; | ||
| lastCodexSeedAttemptAtMs = nowMs; |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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"); |
There was a problem hiding this comment.
🎯 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.
| 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.
|
Thanks for working on this—usage limits are essential when managing multiple accounts or Plus-tier subs. I’ve been using a custom Some feedback on current state of the PR from limited testing: with three Codex instances sharing one #1732 has useful groundwork for per-instance limits. Querying |
|
This is awesome! I was just mentioning this on a twitter post. So glad to see this PR 🎉🎉🎉 |

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:
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 toaccount-limits.json, and servesserver.getAccountLimits(orchestration read scope).ProviderRuntimeIngestioningestsaccount.rate-limits.updatedbefore 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 onrate_limit_event(in addition to existing single-window events).The web client
useAccountLimitsmerges 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
AccountLimitsServicethat maintains a persisted, server-wide cache of provider rate-limit snapshots instateDir/account-limits.json, surviving restarts and merging the freshest snapshot per provider.account.rate-limits.updatedruntime events on session init and rate-limit telemetry;ProviderRuntimeIngestionhandles these events regardless of thread existence.server.getAccountLimitsRPC (requiringAuthOrchestrationReadScope) exposes the limits summary to clients via a cached atom with a 60s stale time.AccountLimitsSectionto the Usage page and anAccountLimitsHoverCardshown when hovering the sidebar Usage button, both displaying per-provider meters with percent used and reset countdowns.Macroscope summarized 54d006d.
Summary by CodeRabbit