Skip to content

Commit 54d006d

Browse files
t3dotggclaude
andcommitted
fix: post-rebase review findings
- 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 <[email protected]>
1 parent a382330 commit 54d006d

5 files changed

Lines changed: 40 additions & 12 deletions

File tree

‎apps/server/src/provider/Layers/ClaudeAdapter.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3434,15 +3434,19 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
34343434
const emitAccountUsageSnapshot = Effect.fn("emitAccountUsageSnapshot")(function* (
34353435
context: ClaudeSessionContext,
34363436
) {
3437-
const fetchUsage = context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
3438-
if (!fetchUsage) return;
3437+
if (!context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET) return;
34393438
const now = yield* Clock.currentTimeMillis;
3440-
if (now - lastAccountUsageFetchAtMs < ACCOUNT_USAGE_MIN_INTERVAL_MS) return;
3439+
const elapsed = now - lastAccountUsageFetchAtMs;
3440+
// A negative elapsed means the wall clock stepped backwards; treat it as
3441+
// expired rather than freezing refreshes until the clock catches up.
3442+
if (elapsed >= 0 && elapsed < ACCOUNT_USAGE_MIN_INTERVAL_MS) return;
34413443
lastAccountUsageFetchAtMs = now;
34423444

34433445
const usage = yield* Effect.promise(async () => {
34443446
try {
3445-
return await fetchUsage();
3447+
// Called through the query object so the SDK method keeps its
3448+
// receiver; an extracted reference loses `this` and throws.
3449+
return await context.query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET?.();
34463450
} catch {
34473451
return undefined;
34483452
}

‎apps/server/src/usage/accountLimitsTranscripts.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,18 @@ export async function readLatestCodexRateLimits(
4848
return null;
4949
}
5050
const newestFirst = [...files].sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, MAX_FILES);
51+
// The newest-mtime file does not necessarily hold the newest snapshot: a
52+
// session can keep appending non-token events after its last rate-limit
53+
// line. A file's snapshot can never be newer than the file's mtime, so
54+
// keep scanning only while a remaining file's mtime could still beat the
55+
// best snapshot found - which usually stops after one or two files.
56+
let best: CodexTranscriptRateLimits | null = null;
5157
for (const file of newestFirst) {
58+
if (best !== null && file.mtimeMs <= best.asOfMs) break;
5259
const found = await readTailRateLimits(file.path, file.mtimeMs);
53-
if (found) return found;
60+
if (found !== null && (best === null || found.asOfMs > best.asOfMs)) best = found;
5461
}
55-
return null;
62+
return best;
5663
}
5764

5865
function isRecord(value: unknown): value is Record<string, unknown> {

‎apps/web/src/components/usage/AccountLimits.tsx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function SnapshotAge({ snapshot, nowMs }: { snapshot: AccountLimitsSnapshot; now
6969

7070
/** Compact per-provider availability, shown on hovering the Usage button. */
7171
export function AccountLimitsHoverCard() {
72-
const { snapshots, isPending } = useAccountLimits();
72+
const { snapshots, isPending, isSettling } = useAccountLimits();
7373
const nowMs = useNowMs();
7474

7575
if (isPending && snapshots.size === 0) {
@@ -93,7 +93,9 @@ export function AccountLimitsHoverCard() {
9393
</span>
9494
</div>
9595
{snapshot === undefined || snapshot.windows.length === 0 ? (
96-
<p className="text-[11px] text-muted-foreground">No limit data yet</p>
96+
<p className="text-[11px] text-muted-foreground">
97+
{snapshot === undefined && isSettling ? "Loading…" : "No limit data yet"}
98+
</p>
9799
) : (
98100
snapshot.windows.map((window) => (
99101
<div key={window.id} className="flex items-center gap-2">
@@ -128,7 +130,7 @@ export function AccountLimitsHoverCard() {
128130

129131
/** The "Limits" strip above the analytics: one column per provider. */
130132
export function AccountLimitsSection() {
131-
const { snapshots, isPending } = useAccountLimits();
133+
const { snapshots, isSettling } = useAccountLimits();
132134
const nowMs = useNowMs();
133135

134136
return (
@@ -153,7 +155,7 @@ export function AccountLimitsSection() {
153155
</div>
154156
{snapshot === undefined || snapshot.windows.length === 0 ? (
155157
<p className="text-xs text-muted-foreground">
156-
{isPending && snapshot === undefined ? "Loading…" : "No limit data yet"}
158+
{snapshot === undefined && isSettling ? "Loading…" : "No limit data yet"}
157159
</p>
158160
) : (
159161
snapshot.windows.map((window) => {

‎apps/web/src/components/usage/UsagePage.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { UsageProviderKind } from "@t3tools/contracts";
22
import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
33
import { ArrowLeftIcon, CheckIcon, RefreshCwIcon, XIcon } from "lucide-react";
4-
import { useMemo, useState } from "react";
4+
import { useCallback, useMemo, useState } from "react";
55

66
import { cn } from "../../lib/utils";
7+
import { useAccountLimits } from "../../state/accountLimits";
78
import { useUsage, type EnvironmentUsageStatus } from "../../state/usage";
89
import {
910
enumerateDays,
@@ -36,7 +37,14 @@ export function UsagePage() {
3637
// Recomputed only when the window length changes, so a re-render does not
3738
// shift the range and refetch every environment.
3839
const window = useMemo(() => makeWindow(windowDays), [windowDays]);
39-
const { merged, environments, isPending, isPartial, refresh } = useUsage(window);
40+
const { merged, environments, isPending, isPartial, refresh: refreshUsage } = useUsage(window);
41+
const { refresh: refreshLimits } = useAccountLimits();
42+
// One refresh button, two caches: the transcript scan and the limits
43+
// snapshot both re-read, or the Limits strip lags the rest of the page.
44+
const refresh = useCallback(() => {
45+
refreshUsage();
46+
refreshLimits();
47+
}, [refreshUsage, refreshLimits]);
4048

4149
// Hold the content until every environment is terminal. Rendering merged
4250
// totals while devices are still answering makes every number on the page

‎apps/web/src/state/accountLimits.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export interface AccountLimitsView {
5151
readonly snapshots: ReadonlyMap<UsageProviderKind, AccountLimitsSnapshot>;
5252
/** True until at least one environment has answered. */
5353
readonly isPending: boolean;
54+
/**
55+
* True while any environment is still answering. A provider with no
56+
* snapshot is "loading" while this holds and "no data" once it clears -
57+
* the first environment to answer must not decide that for the rest.
58+
*/
59+
readonly isSettling: boolean;
5460
readonly refresh: () => void;
5561
}
5662

@@ -87,6 +93,7 @@ export function useAccountLimits(): AccountLimitsView {
8793
return {
8894
snapshots,
8995
isPending: answered === 0 && stillReporting > 0,
96+
isSettling: stillReporting > 0,
9097
refresh,
9198
};
9299
}

0 commit comments

Comments
 (0)