From 82b342e77bf37a04e1d8ba319619af04af5549d8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 9 Jan 2026 09:48:31 +0000 Subject: [PATCH] fix: respect auth cooldown with auth.order --- CHANGELOG.md | 1 + src/agents/auth-profiles.test.ts | 21 ++++++++++++++++++ src/agents/auth-profiles.ts | 37 ++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d9a210e8..e0d6aa845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Commands: accept /models as an alias for /model. - Debugging: add raw model stream logging flags and document gateway watch mode. - Agent: add claude-cli/opus-4.5 runner via Claude CLI with resume support (tools disabled). +- Auth: respect cooldown tracking even with explicit `auth.order` (avoid repeatedly trying known-bad keys). — thanks @steipete - CLI: move `clawdbot message` to subcommands (`message send|poll|…`), fold Discord/Slack/Telegram/WhatsApp tools into `message`, and require `--provider` unless only one provider is configured. - CLI: improve `logs` output (pretty/plain/JSONL), add gateway unreachable hint, and document logging. - WhatsApp: route queued replies to the original sender instead of the bot's own number. (#534) — thanks @mcinteerj diff --git a/src/agents/auth-profiles.test.ts b/src/agents/auth-profiles.test.ts index 0c582e7bc..144471f48 100644 --- a/src/agents/auth-profiles.test.ts +++ b/src/agents/auth-profiles.test.ts @@ -130,6 +130,27 @@ describe("resolveAuthProfileOrder", () => { expect(order).toEqual(["anthropic:work", "anthropic:default"]); }); + it("pushes cooldown profiles to the end even with configured order", () => { + const now = Date.now(); + const order = resolveAuthProfileOrder({ + cfg: { + auth: { + order: { anthropic: ["anthropic:default", "anthropic:work"] }, + profiles: cfg.auth.profiles, + }, + }, + store: { + ...store, + usageStats: { + "anthropic:default": { cooldownUntil: now + 60_000 }, + "anthropic:work": { lastUsed: 1 }, + }, + }, + provider: "anthropic", + }); + expect(order).toEqual(["anthropic:work", "anthropic:default"]); + }); + it("normalizes z.ai aliases in auth.order", () => { const order = resolveAuthProfileOrder({ cfg: { diff --git a/src/agents/auth-profiles.ts b/src/agents/auth-profiles.ts index 780d476d8..fff259824 100644 --- a/src/agents/auth-profiles.ts +++ b/src/agents/auth-profiles.ts @@ -897,14 +897,37 @@ export function resolveAuthProfileOrder(params: { // If user specified explicit order in config, respect it exactly if (configuredOrder && configuredOrder.length > 0) { - // Still put preferredProfile first if specified - if (preferredProfile && deduped.includes(preferredProfile)) { - return [ - preferredProfile, - ...deduped.filter((e) => e !== preferredProfile), - ]; + // ...but still respect cooldown tracking to avoid repeatedly selecting a + // known-bad/rate-limited key as the first candidate. + const now = Date.now(); + const available: string[] = []; + const inCooldown: Array<{ profileId: string; cooldownUntil: number }> = []; + + for (const profileId of deduped) { + const cooldownUntil = store.usageStats?.[profileId]?.cooldownUntil; + if ( + typeof cooldownUntil === "number" && + Number.isFinite(cooldownUntil) && + cooldownUntil > 0 && + now < cooldownUntil + ) { + inCooldown.push({ profileId, cooldownUntil }); + } else { + available.push(profileId); + } } - return deduped; + + const cooldownSorted = inCooldown + .sort((a, b) => a.cooldownUntil - b.cooldownUntil) + .map((entry) => entry.profileId); + + const ordered = [...available, ...cooldownSorted]; + + // Still put preferredProfile first if specified + if (preferredProfile && ordered.includes(preferredProfile)) { + return [preferredProfile, ...ordered.filter((e) => e !== preferredProfile)]; + } + return ordered; } // Otherwise, use round-robin: sort by lastUsed (oldest first)