Skip to content

Commit 7410d4c

Browse files
committed
Merge branch 'release/pr-324' into release/integration-1.2.2
2 parents 7c06d45 + 7f512c3 commit 7410d4c

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

lib/accounts.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,25 @@ export class AccountManager {
659659
const quotaKey = model ? `${family}:${model}` : family;
660660
const healthTracker = getHealthTracker();
661661
healthTracker.recordSuccess(account.index, quotaKey);
662+
const hadCooldownMetadata =
663+
account.coolingDownUntil !== undefined || account.cooldownReason !== undefined;
664+
const hadAuthFailures = (account.consecutiveAuthFailures ?? 0) > 0;
665+
const isCoolingDown = this.isAccountCoolingDown(account);
666+
let healed = false;
667+
668+
if (!isCoolingDown && hadCooldownMetadata) {
669+
this.clearAccountCooldown(account);
670+
healed = true;
671+
}
672+
673+
if (!isCoolingDown && hadAuthFailures) {
674+
this.clearAuthFailures(account);
675+
healed = true;
676+
}
677+
678+
if (healed) {
679+
this.saveToDiskDebounced();
680+
}
662681
}
663682

664683
recordRateLimit(account: ManagedAccount, family: ModelFamily, model?: string | null): void {

test/accounts.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2316,6 +2316,111 @@ describe("AccountManager", () => {
23162316
expect(score).toBe(100);
23172317
});
23182318

2319+
it("recordSuccess clears stale auth failure state and persists the healed account", async () => {
2320+
const { saveAccounts } = await import("../lib/storage.js");
2321+
const mockSaveAccounts = vi.mocked(saveAccounts);
2322+
mockSaveAccounts.mockClear();
2323+
2324+
const now = Date.now();
2325+
const stored = {
2326+
version: 3 as const,
2327+
activeIndex: 0,
2328+
accounts: [
2329+
{
2330+
refreshToken: "token-1",
2331+
addedAt: now,
2332+
lastUsed: now,
2333+
consecutiveAuthFailures: 2,
2334+
coolingDownUntil: now - 1000,
2335+
cooldownReason: "network-error" as const,
2336+
},
2337+
],
2338+
};
2339+
2340+
const manager = new AccountManager(undefined, stored);
2341+
const account = manager.getCurrentAccount()!;
2342+
account.consecutiveAuthFailures = 2;
2343+
2344+
manager.recordSuccess(account, "codex", "gpt-5.1");
2345+
await manager.flushPendingSave();
2346+
2347+
expect(account.consecutiveAuthFailures).toBe(0);
2348+
expect(account.coolingDownUntil).toBeUndefined();
2349+
expect(account.cooldownReason).toBeUndefined();
2350+
expect(mockSaveAccounts).toHaveBeenCalledTimes(1);
2351+
const persisted = mockSaveAccounts.mock.calls[0]?.[0];
2352+
expect(persisted?.accounts[0]?.consecutiveAuthFailures ?? 0).toBe(0);
2353+
expect(persisted?.accounts[0]?.coolingDownUntil).toBeUndefined();
2354+
expect(persisted?.accounts[0]?.cooldownReason).toBeUndefined();
2355+
});
2356+
2357+
it("recordSuccess clears stale cooldown metadata when only the reason remains", async () => {
2358+
const { saveAccounts } = await import("../lib/storage.js");
2359+
const mockSaveAccounts = vi.mocked(saveAccounts);
2360+
mockSaveAccounts.mockClear();
2361+
2362+
const now = Date.now();
2363+
const stored = {
2364+
version: 3 as const,
2365+
activeIndex: 0,
2366+
accounts: [
2367+
{
2368+
refreshToken: "token-1",
2369+
addedAt: now,
2370+
lastUsed: now,
2371+
cooldownReason: "network-error" as const,
2372+
},
2373+
],
2374+
};
2375+
2376+
const manager = new AccountManager(undefined, stored);
2377+
const account = manager.getCurrentAccount()!;
2378+
2379+
manager.recordSuccess(account, "codex", "gpt-5.1");
2380+
await manager.flushPendingSave();
2381+
2382+
expect(account.coolingDownUntil).toBeUndefined();
2383+
expect(account.cooldownReason).toBeUndefined();
2384+
expect(mockSaveAccounts).toHaveBeenCalledTimes(1);
2385+
const persisted = mockSaveAccounts.mock.calls[0]?.[0];
2386+
expect(persisted?.accounts[0]?.coolingDownUntil).toBeUndefined();
2387+
expect(persisted?.accounts[0]?.cooldownReason).toBeUndefined();
2388+
});
2389+
2390+
it("recordSuccess does not clear an active cooldown from a newer concurrent failure", async () => {
2391+
const { saveAccounts } = await import("../lib/storage.js");
2392+
const mockSaveAccounts = vi.mocked(saveAccounts);
2393+
mockSaveAccounts.mockClear();
2394+
2395+
const now = Date.now();
2396+
const stored = {
2397+
version: 3 as const,
2398+
activeIndex: 0,
2399+
accounts: [
2400+
{
2401+
refreshToken: "token-1",
2402+
addedAt: now,
2403+
lastUsed: now,
2404+
consecutiveAuthFailures: 2,
2405+
coolingDownUntil: now + 60_000,
2406+
cooldownReason: "auth-failure" as const,
2407+
},
2408+
],
2409+
};
2410+
2411+
const manager = new AccountManager(undefined, stored);
2412+
const account = manager.getCurrentAccount()!;
2413+
account.consecutiveAuthFailures = 2;
2414+
2415+
manager.recordSuccess(account, "codex", "gpt-5.1");
2416+
await manager.flushPendingSave();
2417+
2418+
expect(account.consecutiveAuthFailures).toBe(2);
2419+
expect(account.coolingDownUntil).toBe(now + 60_000);
2420+
expect(account.cooldownReason).toBe("auth-failure");
2421+
expect(mockSaveAccounts).not.toHaveBeenCalled();
2422+
});
2423+
23192424
it("recordRateLimit updates health and drains token bucket", () => {
23202425
const now = Date.now();
23212426
const stored = {

0 commit comments

Comments
 (0)