rate limit the quota update: once every 2 min

This commit is contained in:
nhat.nguyenhong 2026-09-24 13:52:50 +00:00
parent a1fe7c7a87
commit ac29d26e12
2 changed files with 21 additions and 2 deletions

View File

@ -238,7 +238,8 @@ instead of being shown as zero.
The sidebar refreshes after each completed turn (`session.idle`, plus `session.status`, The sidebar refreshes after each completed turn (`session.idle`, plus `session.status`,
`session.updated`, and `message.updated` as fallbacks since `session.idle` is a server-plugin `session.updated`, and `message.updated` as fallbacks since `session.idle` is a server-plugin
event that may never reach the TUI bus — all debounced) and when the active session changes, event that may never reach the TUI bus — all debounced) and when the active session changes,
plus every 3 minutes as a fallback (`COMMANDCODE_QUOTA_INTERVAL_MS` overrides it) and the but never more than once every 2 minutes; every 5 minutes as a fallback
(`COMMANDCODE_QUOTA_INTERVAL_MS` overrides the fallback interval) and the
countdown ticks every 30 seconds. Key resolution and fetch failures are redacted and shown countdown ticks every 30 seconds. Key resolution and fetch failures are redacted and shown
inline; the panel never stays on `loading…` and never blocks the provider. Extra trace lines inline; the panel never stays on `loading…` and never blocks the provider. Extra trace lines
(`tui-quota`) are appended to the debug log only when debug tracing is on. (`tui-quota`) are appended to the debug log only when debug tracing is on.

View File

@ -69,9 +69,11 @@ type TuiApi = {
const ID = "commandcode-toggles"; const ID = "commandcode-toggles";
const CATEGORY = "CommandCode"; const CATEGORY = "CommandCode";
const SIDEBAR_ORDER = 90; const SIDEBAR_ORDER = 90;
const DEFAULT_QUOTA_INTERVAL_MS = 180_000; const DEFAULT_QUOTA_INTERVAL_MS = 300_000;
const MIN_REFRESH_INTERVAL_MS = 120_000;
// Extra TUI-bus signals that mark the end of a turn. `session.idle` is a server // Extra TUI-bus signals that mark the end of a turn. `session.idle` is a server
// plugin event and may never fire here; these keep the panel fresh regardless. // plugin event and may never fire here; these keep the panel fresh regardless.
// All event-driven refreshes are floored to MIN_REFRESH_INTERVAL_MS (hard skip).
const QUOTA_TRIGGER_EVENTS = ["session.idle", "session.status", "session.updated", "message.updated"] as const; const QUOTA_TRIGGER_EVENTS = ["session.idle", "session.status", "session.updated", "message.updated"] as const;
function safeEnv(name: string): string | undefined { function safeEnv(name: string): string | undefined {
@ -168,6 +170,11 @@ function quotaIntervalMs(): number {
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_QUOTA_INTERVAL_MS; return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_QUOTA_INTERVAL_MS;
} }
function formatClock(ms: number): string {
if (!(ms > 0)) return "--:--:--";
return new Date(ms).toTimeString().slice(0, 8);
}
function shortWindow(w: QuotaWindow, nowMs: number): string { function shortWindow(w: QuotaWindow, nowMs: number): string {
const label = w.id === "fiveHour" ? "5h" : w.id === "weekly" ? "7d" : "mo"; const label = w.id === "fiveHour" ? "5h" : w.id === "weekly" ? "7d" : "mo";
const reset = formatReset(w.resetAtMs, nowMs); const reset = formatReset(w.resetAtMs, nowMs);
@ -180,9 +187,11 @@ export const tui = async (api: TuiApi): Promise<void> => {
const [toggles, setToggles] = createSignal(readToggles()); const [toggles, setToggles] = createSignal(readToggles());
const [quota, setQuota] = createSignal<QuotaResult | null>(null); const [quota, setQuota] = createSignal<QuotaResult | null>(null);
const [now, setNow] = createSignal(Date.now()); const [now, setNow] = createSignal(Date.now());
const [updatedAt, setUpdatedAt] = createSignal(0);
let inflight = false; let inflight = false;
let pending = false; let pending = false;
let lastRefreshAt = 0;
let debounceTimer: ReturnType<typeof setTimeout> | undefined; let debounceTimer: ReturnType<typeof setTimeout> | undefined;
let lastSessionId: string | undefined; let lastSessionId: string | undefined;
@ -194,6 +203,11 @@ export const tui = async (api: TuiApi): Promise<void> => {
trace("coalesced", "inflight"); trace("coalesced", "inflight");
return; return;
} }
const since = Date.now() - lastRefreshAt;
if (lastRefreshAt > 0 && since < MIN_REFRESH_INTERVAL_MS) {
trace("rate-limited", `remainingMs=${MIN_REFRESH_INTERVAL_MS - since}`);
return;
}
inflight = true; inflight = true;
try { try {
const { key: apiKey, source } = resolveApiKeySource(api); const { key: apiKey, source } = resolveApiKeySource(api);
@ -222,7 +236,9 @@ export const tui = async (api: TuiApi): Promise<void> => {
trace("fetch-throw", message); trace("fetch-throw", message);
} finally { } finally {
inflight = false; inflight = false;
lastRefreshAt = Date.now();
setNow(Date.now()); setNow(Date.now());
setUpdatedAt(Date.now());
if (pending) { if (pending) {
pending = false; pending = false;
trace("flush-pending"); trace("flush-pending");
@ -284,6 +300,7 @@ export const tui = async (api: TuiApi): Promise<void> => {
) : quota() === null ? ( ) : quota() === null ? (
<text fg={theme.current.textMuted}>quota: loading…</text> <text fg={theme.current.textMuted}>quota: loading…</text>
) : null} ) : null}
<text fg={theme.current.textMuted}>updated @ {formatClock(updatedAt())}</text>
</box> </box>
); );
}; };
@ -365,6 +382,7 @@ export const tui = async (api: TuiApi): Promise<void> => {
} }
setQuota(result); setQuota(result);
setNow(Date.now()); setNow(Date.now());
setUpdatedAt(Date.now());
api.ui.toast({ title: CATEGORY, message: formatQuota(result.quota), variant: "info", duration: 15000 }); api.ui.toast({ title: CATEGORY, message: formatQuota(result.quota), variant: "info", duration: 15000 });
}, },
}, },