From ac29d26e12600568dcabf6e08fcc2cbf0e3cdc70 Mon Sep 17 00:00:00 2001 From: "nhat.nguyenhong" Date: Thu, 24 Sep 2026 13:52:50 +0000 Subject: [PATCH] rate limit the quota update: once every 2 min --- README.md | 3 ++- src/tui.tsx | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9375f44..3894a37 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,8 @@ instead of being shown as zero. 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 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 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. diff --git a/src/tui.tsx b/src/tui.tsx index c161160..aecb81e 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -69,9 +69,11 @@ type TuiApi = { const ID = "commandcode-toggles"; const CATEGORY = "CommandCode"; 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 // 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; function safeEnv(name: string): string | undefined { @@ -168,6 +170,11 @@ function quotaIntervalMs(): number { 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 { const label = w.id === "fiveHour" ? "5h" : w.id === "weekly" ? "7d" : "mo"; const reset = formatReset(w.resetAtMs, nowMs); @@ -180,9 +187,11 @@ export const tui = async (api: TuiApi): Promise => { const [toggles, setToggles] = createSignal(readToggles()); const [quota, setQuota] = createSignal(null); const [now, setNow] = createSignal(Date.now()); + const [updatedAt, setUpdatedAt] = createSignal(0); let inflight = false; let pending = false; + let lastRefreshAt = 0; let debounceTimer: ReturnType | undefined; let lastSessionId: string | undefined; @@ -194,6 +203,11 @@ export const tui = async (api: TuiApi): Promise => { trace("coalesced", "inflight"); 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; try { const { key: apiKey, source } = resolveApiKeySource(api); @@ -222,7 +236,9 @@ export const tui = async (api: TuiApi): Promise => { trace("fetch-throw", message); } finally { inflight = false; + lastRefreshAt = Date.now(); setNow(Date.now()); + setUpdatedAt(Date.now()); if (pending) { pending = false; trace("flush-pending"); @@ -284,6 +300,7 @@ export const tui = async (api: TuiApi): Promise => { ) : quota() === null ? ( quota: loading… ) : null} + updated @ {formatClock(updatedAt())} ); }; @@ -365,6 +382,7 @@ export const tui = async (api: TuiApi): Promise => { } setQuota(result); setNow(Date.now()); + setUpdatedAt(Date.now()); api.ui.toast({ title: CATEGORY, message: formatQuota(result.quota), variant: "info", duration: 15000 }); }, },