diff --git a/CHANGELOG.md b/CHANGELOG.md index 31eaf0a02dc..eae5c0167fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -175,6 +175,7 @@ Docs: https://docs.openclaw.ai - Tests/Telegram: add regression coverage for command-menu sync that asserts all `setMyCommands` entries are Telegram-safe and hyphen-normalized across native/custom/plugin command sources. (#19703) Thanks @obviyus. - Agents/Image: collapse resize diagnostics to one line per image and include visible pixel/byte size details in the log message for faster triage. +- Auth/Cooldowns: clear all usage stats fields (`disabledUntil`, `disabledReason`, `failureCounts`) in `clearAuthProfileCooldown` so manual cooldown resets fully recover billing-disabled profiles without requiring direct file edits. (#19211) Thanks @nabbilkhan. - Agents/Subagents: preemptively guard accumulated tool-result context before model calls by truncating oversized outputs and compacting oldest tool-result messages to avoid context-window overflow crashes. Thanks @tyler6204. - Agents/Subagents/CLI: fail `sessions_spawn` when subagent model patching is rejected, allow subagent model patch defaults from `subagents.model`, and keep `sessions list`/`status` model reporting aligned to runtime model resolution. (#18660) Thanks @robbyczgw-cla. - Agents/Subagents: add explicit subagent guidance to recover from `[compacted: tool output removed to free context]` / `[truncated: output exceeded context limit]` markers by re-reading with smaller chunks instead of full-file `cat`. Thanks @tyler6204. diff --git a/src/agents/auth-profiles/usage.test.ts b/src/agents/auth-profiles/usage.test.ts index a4f3f5261ae..128eb35e560 100644 --- a/src/agents/auth-profiles/usage.test.ts +++ b/src/agents/auth-profiles/usage.test.ts @@ -1,11 +1,21 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "./types.js"; import { + clearAuthProfileCooldown, clearExpiredCooldowns, isProfileInCooldown, resolveProfileUnusableUntil, } from "./usage.js"; +vi.mock("./store.js", async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + updateAuthProfileStoreWithLock: vi.fn().mockResolvedValue(null), + saveAuthProfileStore: vi.fn(), + }; +}); + function makeStore(usageStats: AuthProfileStore["usageStats"]): AuthProfileStore { return { version: 1, @@ -283,3 +293,55 @@ describe("clearExpiredCooldowns", () => { expect(clearExpiredCooldowns(store)).toBe(false); }); }); + +// --------------------------------------------------------------------------- +// clearAuthProfileCooldown +// --------------------------------------------------------------------------- + +describe("clearAuthProfileCooldown", () => { + it("clears all error state fields including disabledUntil and failureCounts", async () => { + const store = makeStore({ + "anthropic:default": { + cooldownUntil: Date.now() + 60_000, + disabledUntil: Date.now() + 3_600_000, + disabledReason: "billing", + errorCount: 5, + failureCounts: { billing: 3, rate_limit: 2 }, + }, + }); + + await clearAuthProfileCooldown({ store, profileId: "anthropic:default" }); + + const stats = store.usageStats?.["anthropic:default"]; + expect(stats?.cooldownUntil).toBeUndefined(); + expect(stats?.disabledUntil).toBeUndefined(); + expect(stats?.disabledReason).toBeUndefined(); + expect(stats?.errorCount).toBe(0); + expect(stats?.failureCounts).toBeUndefined(); + }); + + it("preserves lastUsed and lastFailureAt timestamps", async () => { + const lastUsed = Date.now() - 10_000; + const lastFailureAt = Date.now() - 5_000; + const store = makeStore({ + "anthropic:default": { + cooldownUntil: Date.now() + 60_000, + errorCount: 3, + lastUsed, + lastFailureAt, + }, + }); + + await clearAuthProfileCooldown({ store, profileId: "anthropic:default" }); + + const stats = store.usageStats?.["anthropic:default"]; + expect(stats?.lastUsed).toBe(lastUsed); + expect(stats?.lastFailureAt).toBe(lastFailureAt); + }); + + it("no-ops for unknown profile id", async () => { + const store = makeStore(undefined); + await clearAuthProfileCooldown({ store, profileId: "nonexistent" }); + expect(store.usageStats).toBeUndefined(); + }); +}); diff --git a/src/agents/auth-profiles/usage.ts b/src/agents/auth-profiles/usage.ts index 20c5b3b0e14..1bfda226873 100644 --- a/src/agents/auth-profiles/usage.ts +++ b/src/agents/auth-profiles/usage.ts @@ -400,6 +400,9 @@ export async function clearAuthProfileCooldown(params: { ...freshStore.usageStats[profileId], errorCount: 0, cooldownUntil: undefined, + disabledUntil: undefined, + disabledReason: undefined, + failureCounts: undefined, }; return true; }, @@ -416,6 +419,9 @@ export async function clearAuthProfileCooldown(params: { ...store.usageStats[profileId], errorCount: 0, cooldownUntil: undefined, + disabledUntil: undefined, + disabledReason: undefined, + failureCounts: undefined, }; saveAuthProfileStore(store, agentDir); }