diff --git a/packages/coding-agent/src/modes/components/status-line/component.ts b/packages/coding-agent/src/modes/components/status-line/component.ts index 2886dc9ec..9f4bb4872 100644 --- a/packages/coding-agent/src/modes/components/status-line/component.ts +++ b/packages/coding-agent/src/modes/components/status-line/component.ts @@ -962,10 +962,8 @@ export class StatusLineComponent implements Component { return this.#vibeWorkerTokenRate?.() ?? null; } - #getUsageContextKey(session: AgentSession): string { - const activeProvider = session.state.model?.provider ?? session.model?.provider ?? ""; + #formatUsageContextKey(activeProvider: string | undefined, identity: OAuthAccountIdentity | undefined): string { if (!activeProvider) return ""; - const identity = session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId); // orgId is part of the key: rotating between two same-email Anthropic // subscriptions must invalidate the cached usage immediately instead of // showing the previous org's quota for the rest of the cache TTL. @@ -978,6 +976,14 @@ export class StatusLineComponent implements Component { ].join("\0"); } + #getUsageContextKey(session: AgentSession): string { + const activeProvider = session.state.model?.provider ?? session.model?.provider; + const identity = activeProvider + ? session.modelRegistry?.authStorage?.getOAuthAccountIdentity(activeProvider, session.sessionId) + : undefined; + return this.#formatUsageContextKey(activeProvider, identity); + } + /** * Startup redraws only arm a short-delayed task; timeout releases the render * cadence while a late successful fetch can still refresh the cached segment. @@ -1042,7 +1048,7 @@ export class StatusLineComponent implements Component { this.#cachedUsage = normalized; this.#usageFetchedAt = Date.now(); if (activeProvider !== "openai-codex" || !normalized) return; - const contextKey = this.#getUsageContextKey(session); + const contextKey = this.#formatUsageContextKey(activeProvider, activeIdentity); const previous = this.#codexResetSnapshots.get(contextKey); this.#codexResetSnapshots.set(contextKey, normalized); if (!previous || !settings.get("tui.codexResetFireworks")) return; diff --git a/packages/coding-agent/test/status-line-usage-refresh.test.ts b/packages/coding-agent/test/status-line-usage-refresh.test.ts index 1b67ef681..e9645cdc6 100644 --- a/packages/coding-agent/test/status-line-usage-refresh.test.ts +++ b/packages/coding-agent/test/status-line-usage-refresh.test.ts @@ -65,8 +65,7 @@ interface CodexUsageState { omitFetchedAt?: boolean; } -function codexUsageReport(state: CodexUsageState): unknown[] { - const accountId = "account-1"; +function codexUsageReport(state: CodexUsageState, accountId = "account-1"): unknown[] { return [ { provider: "openai-codex", @@ -90,7 +89,13 @@ function codexUsageReport(state: CodexUsageState): unknown[] { ]; } -function makeCodexSession(fetchUsageReports: (signal?: AbortSignal) => Promise): AgentSession { +function makeCodexSession( + fetchUsageReports: (signal?: AbortSignal) => Promise, + resolveActiveIdentity: () => { accountId: string; email?: string } = () => ({ + accountId: "account-1", + email: "codex@example.com", + }), +): AgentSession { const session = makeSession(fetchUsageReports) as unknown as Record; session.sessionId = "session-1"; session.state = { @@ -100,10 +105,7 @@ function makeCodexSession(fetchUsageReports: (signal?: AbortSignal) => Promise ({ - accountId: "account-1", - email: "codex@example.com", - }), + getOAuthAccountIdentity: resolveActiveIdentity, }, }; return session as unknown as AgentSession; @@ -323,6 +325,47 @@ describe("StatusLineComponent usage refresh", () => { component.dispose(); }); + it("binds each reset snapshot to the account identity used to normalize it", async () => { + Settings.instance.set("tui.codexResetFireworks", true); + const sevenDayResetAt = Date.now() + 80 * 3_600_000; + const reports = [ + ...codexUsageReport( + { + sevenDayPercent: 18, + sevenDayResetAt, + savedResets: 0, + }, + "account-a", + ), + ...codexUsageReport( + { + sevenDayPercent: 22, + sevenDayResetAt, + savedResets: 1, + }, + "account-b", + ), + ]; + const identityLookups: string[] = []; + const component = new StatusLineComponent( + makeCodexSession( + async () => reports, + () => ({ accountId: identityLookups.shift() ?? "account-a" }), + ), + ); + const events: CodexResetFireworksEvent[] = []; + component.setCodexResetFireworksHandler(event => events.push(event)); + + await refreshUsage(component); + // The refresh starts under A, but B is active when its report is normalized. + // A later identity lookup must not attribute B's saved reset to A. + identityLookups.push("account-a", "account-b", "account-a"); + await refreshUsage(component, 5 * 60_000); + + expect(events).toEqual([]); + component.dispose(); + }); + it("keeps an unavailable saved-reset count unknown across refreshes", async () => { Settings.instance.set("tui.codexResetFireworks", true); const sevenDayResetAt = Date.now() + 80 * 3_600_000;