Skip to content

Commit a01e56d

Browse files
committed
Merge branch 'release/pr-329' into release/integration-1.2.2
# Conflicts: # lib/refresh-guardian.ts # test/refresh-guardian.test.ts
2 parents 7b716ab + 6e6781a commit a01e56d

2 files changed

Lines changed: 320 additions & 98 deletions

File tree

lib/refresh-guardian.ts

Lines changed: 58 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from "./logger.js";
22
import { refreshExpiringAccounts } from "./proactive-refresh.js";
33
import type { AccountManager } from "./accounts.js";
4+
import { ACCOUNT_LIMITS } from "./constants.js";
45
import { CodexAuthError } from "./errors.js";
56
import type { CooldownReason } from "./storage.js";
67
import type { TokenResult } from "./types.js";
@@ -25,80 +26,7 @@ export interface RefreshGuardianStats {
2526
}
2627

2728
const DEFAULT_INTERVAL_MS = 60_000;
28-
29-
function findMatchingLiveAccountIndexes(
30-
liveAccounts: ManagedAccount[],
31-
predicate: (candidate: ManagedAccount) => boolean,
32-
): number[] {
33-
const matches: number[] = [];
34-
for (const [index, candidate] of liveAccounts.entries()) {
35-
if (predicate(candidate)) {
36-
matches.push(index);
37-
}
38-
}
39-
return matches;
40-
}
41-
42-
function resolveLiveAccountIndex(
43-
liveAccounts: ManagedAccount[],
44-
sourceAccount: ManagedAccount,
45-
): number {
46-
if (sourceAccount.accountId) {
47-
const accountIdMatches = findMatchingLiveAccountIndexes(
48-
liveAccounts,
49-
(candidate) => candidate.accountId === sourceAccount.accountId,
50-
);
51-
const resolvedIndex = accountIdMatches[0];
52-
if (resolvedIndex !== undefined) {
53-
log.debug("Resolved refreshed account by accountId", {
54-
sourceIndex: sourceAccount.index,
55-
resolvedIndex,
56-
matchCount: accountIdMatches.length,
57-
});
58-
if (accountIdMatches.length > 1) {
59-
log.warn("Duplicate live accountId matches during refresh reconciliation", {
60-
sourceIndex: sourceAccount.index,
61-
resolvedIndex,
62-
matchCount: accountIdMatches.length,
63-
});
64-
}
65-
return resolvedIndex;
66-
}
67-
}
68-
69-
const sourceEmail = sanitizeEmail(sourceAccount.email);
70-
if (sourceEmail) {
71-
const emailMatches = findMatchingLiveAccountIndexes(
72-
liveAccounts,
73-
(candidate) => sanitizeEmail(candidate.email) === sourceEmail,
74-
);
75-
const resolvedIndex = emailMatches[0];
76-
if (resolvedIndex !== undefined) {
77-
log.debug("Resolved refreshed account by email", {
78-
sourceIndex: sourceAccount.index,
79-
resolvedIndex,
80-
matchCount: emailMatches.length,
81-
});
82-
if (emailMatches.length > 1) {
83-
log.warn("Duplicate live email matches during refresh reconciliation", {
84-
sourceIndex: sourceAccount.index,
85-
resolvedIndex,
86-
matchCount: emailMatches.length,
87-
});
88-
}
89-
return resolvedIndex;
90-
}
91-
}
92-
93-
const byToken = liveAccounts.findIndex(
94-
(candidate) => candidate.refreshToken === sourceAccount.refreshToken,
95-
);
96-
log.debug("Resolved refreshed account by refresh token fallback", {
97-
sourceIndex: sourceAccount.index,
98-
resolvedIndex: byToken,
99-
});
100-
return byToken;
101-
}
29+
const NETWORK_FAILURE_COOLDOWN_MS = 6_000;
10230

10331
export class RefreshGuardian {
10432
private readonly getAccountManager: () => AccountManager | null;
@@ -170,6 +98,15 @@ export class RefreshGuardian {
17098
return "network-error";
17199
}
172100

101+
private getNetworkFailureCooldownMs(): number {
102+
return Math.min(this.bufferMs, NETWORK_FAILURE_COOLDOWN_MS);
103+
}
104+
105+
private getAuthFailureCooldownMs(failureCount: number): number {
106+
const streak = Math.max(1, Math.floor(failureCount));
107+
return Math.min(this.bufferMs, ACCOUNT_LIMITS.AUTH_FAILURE_COOLDOWN_MS * streak);
108+
}
109+
173110
private async applyRefreshOutcome(
174111
manager: AccountManager,
175112
sourceAccount: ReturnType<AccountManager["getAccountsSnapshot"]>[number],
@@ -185,7 +122,12 @@ export class RefreshGuardian {
185122
if (result.tokenResult?.type !== "success") {
186123
const account = manager.getAccountByIdentity(sourceAccount);
187124
if (!account) return false;
188-
manager.markAccountCoolingDown(account, this.bufferMs, "network-error");
125+
manager.clearAuthFailures(account);
126+
manager.markAccountCoolingDown(
127+
account,
128+
this.getNetworkFailureCooldownMs(),
129+
"network-error",
130+
);
189131
this.stats.failed += 1;
190132
this.stats.networkFailed += 1;
191133
return true;
@@ -208,9 +150,10 @@ export class RefreshGuardian {
208150
manager.getAccountByIdentity(sourceAccount, refreshedAuth) ??
209151
manager.getAccountByIdentity(sourceAccount);
210152
if (account) {
153+
manager.clearAuthFailures(account);
211154
manager.markAccountCoolingDown(
212155
account,
213-
this.bufferMs,
156+
this.getNetworkFailureCooldownMs(),
214157
"network-error",
215158
);
216159
}
@@ -231,7 +174,21 @@ export class RefreshGuardian {
231174
? "auth-failure"
232175
: "network-error";
233176
if (account) {
234-
manager.markAccountCoolingDown(account, this.bufferMs, cooldownReason);
177+
if (cooldownReason === "auth-failure") {
178+
const failureCount = manager.incrementAuthFailures(account);
179+
manager.markAccountCoolingDown(
180+
account,
181+
this.getAuthFailureCooldownMs(failureCount),
182+
cooldownReason,
183+
);
184+
} else {
185+
manager.clearAuthFailures(account);
186+
manager.markAccountCoolingDown(
187+
account,
188+
this.getNetworkFailureCooldownMs(),
189+
cooldownReason,
190+
);
191+
}
235192
}
236193
this.stats.failed += 1;
237194
if (cooldownReason === "auth-failure") this.stats.authFailed += 1;
@@ -245,7 +202,24 @@ export class RefreshGuardian {
245202
const account = manager.getAccountByIdentity(sourceAccount);
246203
if (!account) return false;
247204
const cooldownReason = this.classifyFailureReason(result.tokenResult);
248-
manager.markAccountCoolingDown(account, this.bufferMs, cooldownReason);
205+
if (cooldownReason === "rate-limit") {
206+
manager.clearAuthFailures(account);
207+
manager.markRateLimited(account, this.bufferMs, "codex");
208+
} else if (cooldownReason === "auth-failure") {
209+
const failureCount = manager.incrementAuthFailures(account);
210+
manager.markAccountCoolingDown(
211+
account,
212+
this.getAuthFailureCooldownMs(failureCount),
213+
cooldownReason,
214+
);
215+
} else {
216+
manager.clearAuthFailures(account);
217+
manager.markAccountCoolingDown(
218+
account,
219+
this.getNetworkFailureCooldownMs(),
220+
cooldownReason,
221+
);
222+
}
249223
this.stats.failed += 1;
250224
if (cooldownReason === "rate-limit") this.stats.rateLimited += 1;
251225
else if (cooldownReason === "auth-failure") this.stats.authFailed += 1;
@@ -258,7 +232,12 @@ export class RefreshGuardian {
258232
case "no_refresh_token": {
259233
const account = manager.getAccountByIdentity(sourceAccount);
260234
if (!account) return false;
261-
manager.markAccountCoolingDown(account, this.bufferMs, "auth-failure");
235+
const failureCount = manager.incrementAuthFailures(account);
236+
manager.markAccountCoolingDown(
237+
account,
238+
this.getAuthFailureCooldownMs(failureCount),
239+
"auth-failure",
240+
);
262241
manager.setAccountEnabled(account.index, false);
263242
this.stats.noRefreshToken += 1;
264243
this.stats.failed += 1;

0 commit comments

Comments
 (0)