From a10fe079ce7fda5f6e75b7697a3c07eaf3e6782e Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 28 Jul 2026 11:47:08 +0200 Subject: [PATCH] fix(coding-agent): suppressed credential disable tombstones for active accounts - Update isActionableDisable in usage-cli.ts to accept active accounts and check identity matches. - Suppress credential disable tombstones when an active account exists for the same provider and identity. - Add test coverage for suppressing tombstones when active accounts share the same identity. --- packages/coding-agent/src/cli/usage-cli.ts | 33 +++++++++++++++++--- packages/coding-agent/test/usage-cli.test.ts | 29 +++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/src/cli/usage-cli.ts b/packages/coding-agent/src/cli/usage-cli.ts index f7ace2341..a15d96c74 100644 --- a/packages/coding-agent/src/cli/usage-cli.ts +++ b/packages/coding-agent/src/cli/usage-cli.ts @@ -560,9 +560,34 @@ function formatReloginDeadline( * automatically (refresh failure, upstream invalidation). Rows the user * replaced or deleted deliberately are lifecycle noise, not lost capacity. */ -function isActionableDisable(summary: DisabledCredentialSummary): boolean { +function isActionableDisable(summary: DisabledCredentialSummary, activeAccounts: UsageAccountIdentity[] = []): boolean { if (summary.type !== "oauth") return false; - return !/^(replaced by|deleted by user)/i.test(summary.cause); + if (/^(replaced by|deleted by user)/i.test(summary.cause)) return false; + + // Do not display tombstone if there is an active account for the same provider + // matching the same identity (email, accountId, or org). + const summaryEmail = summary.email?.toLowerCase(); + const summaryAccountId = summary.accountId?.toLowerCase(); + const summaryOrgId = summary.orgId?.toLowerCase(); + + const matchesActive = activeAccounts.some(account => { + if (account.provider !== summary.provider) return false; + + const accountEmail = account.email?.toLowerCase(); + const accountAccountId = account.accountId?.toLowerCase(); + const accountOrgId = account.orgId?.toLowerCase(); + + // If email or accountId match, it's the same identity + if (summaryEmail && accountEmail && summaryEmail === accountEmail) return true; + if (summaryAccountId && accountAccountId && summaryAccountId === accountAccountId) return true; + + // Fallback: if orgId matches and neither email nor accountId contradicts + if (summaryOrgId && accountOrgId && summaryOrgId === accountOrgId) return true; + + return false; + }); + + return !matchesActive; } /** Human-sized disable cause: the upstream `error_description` when embedded, else the first clause. */ @@ -610,7 +635,7 @@ export function formatUsageBreakdown( } const disabledByProvider = new Map(); for (const summary of disabled) { - if (!isActionableDisable(summary)) continue; + if (!isActionableDisable(summary, accounts)) continue; const list = disabledByProvider.get(summary.provider) ?? []; list.push(summary); disabledByProvider.set(summary.provider, list); @@ -1018,7 +1043,7 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise { const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider)); if (stats.length > 0) capacity[report.provider] = stats; } - let disabledForJson = disabled.filter(isActionableDisable); + let disabledForJson = disabled.filter(summary => isActionableDisable(summary, accounts)); if (redaction) { disabledForJson = disabledForJson.map(summary => ({ ...summary, diff --git a/packages/coding-agent/test/usage-cli.test.ts b/packages/coding-agent/test/usage-cli.test.ts index efc6b6f58..b6f58ec16 100644 --- a/packages/coding-agent/test/usage-cli.test.ts +++ b/packages/coding-agent/test/usage-cli.test.ts @@ -352,6 +352,35 @@ describe("formatUsageBreakdown", () => { expect(text).not.toContain("rotated@example.test"); expect(text).not.toContain("Fireworks"); }); + it("suppresses auto-disabled tombstones when an active account exists with the same identity", () => { + const now = Date.now(); + const activeAccounts: UsageAccountIdentity[] = [ + { + provider: "anthropic", + type: "oauth", + email: "active@example.test", + }, + ]; + const disabled = [ + { + id: 30, + provider: "anthropic", + type: "oauth" as const, + email: "active@example.test", + cause: "oauth refresh failed: Refresh token expired", + }, + { + id: 31, + provider: "anthropic", + type: "oauth" as const, + email: "truly-dead@example.test", + cause: "oauth refresh failed: Refresh token expired", + }, + ]; + const text = stripVTControlCharacters(formatUsageBreakdown([], activeAccounts, now, undefined, disabled)); + expect(text).not.toContain("active@example.test — disabled"); + expect(text).toContain("✗ truly-dead@example.test — disabled"); + }); it("renders a tombstone-only provider section even when no active credential remains", () => { const disabled = [