From f5b4020a856a98dfa6533023b3bd22926be58211 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Mon, 31 Aug 2026 10:45:35 +0900 Subject: [PATCH] fix(kiro): honor exhausted account cooldown on rotation --- src/oauth/generic-account-failover.ts | 2 +- tests/kiro-pool-rank.test.ts | 34 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/oauth/generic-account-failover.ts b/src/oauth/generic-account-failover.ts index a36974a629..4674dc5c1f 100644 --- a/src/oauth/generic-account-failover.ts +++ b/src/oauth/generic-account-failover.ts @@ -206,7 +206,7 @@ export function rotateGenericOAuthAccountOn429( // An account whose allowance is provably spent gets a reset-aligned cooldown instead of // the default minute: retrying it every 60s until the window rolls over is pure waste. // A Retry-After from upstream still wins — it is the server's own instruction. - const exhausted = parsed === null ? exhaustedCooldownMs(providerName, failedAccountId, now) : null; + const exhausted = parsed === undefined ? exhaustedCooldownMs(providerName, failedAccountId, now) : null; const cooldownMs = exhausted ?? Math.min(parsed ?? DEFAULT_COOLDOWN_MS, MAX_COOLDOWN_MS); health.set(healthKey(providerName, failedAccountId), { cooldownUntil: now + cooldownMs, diff --git a/tests/kiro-pool-rank.test.ts b/tests/kiro-pool-rank.test.ts index 4dfe1491e2..7e2023b3d2 100644 --- a/tests/kiro-pool-rank.test.ts +++ b/tests/kiro-pool-rank.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { clearGenericFailoverHealth, + eligibleFailoverAccounts, forgetGenericFailoverRoster, preferredInitialAccount, rotateGenericOAuthAccountOn429, @@ -106,6 +107,39 @@ describe("headroom ranking", () => { }); describe("exhaustion cooldown", () => { + test("rotation uses the reset-aligned cooldown when Retry-After is absent", async () => { + const originalHome = process.env.OPENCODEX_HOME; + const home = mkdtempSync(join(tmpdir(), "ocx-kiro-exhaustion-")); + process.env.OPENCODEX_HOME = home; + clearGenericFailoverHealth(); + try { + for (let i = 0; i < 2; i++) { + await saveCredential("kiro", { + access: `access-${i}`, + refresh: `refresh-${i}`, + expires: Date.now() + 3_600_000, + accountId: `uuid-${i}`, + } as never, { addAccount: true }); + } + const ids = getAccountSet("kiro")?.accounts.map(account => account.id) ?? []; + const now = Date.now(); + seedExhausted(ids[0]!, now + 60 * 60_000); + const config = { + providers: { + kiro: { adapter: "openai-chat", authMode: "oauth" }, + }, + } as unknown as OcxConfig; + + expect(rotateGenericOAuthAccountOn429(config, "kiro", ids[0]!, null, now)).toBe(ids[1]); + expect(eligibleFailoverAccounts("kiro", now + 60_001)).toEqual([ids[1]!]); + } finally { + clearGenericFailoverHealth(); + if (originalHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = originalHome; + rmSync(home, { recursive: true, force: true }); + } + }); + test("a distant reset is clamped to a day", () => { const now = Date.now(); seedExhausted("a", now + 3 * 24 * 60 * 60_000);