11import { createLogger } from "./logger.js" ;
22import { refreshExpiringAccounts } from "./proactive-refresh.js" ;
33import type { AccountManager } from "./accounts.js" ;
4+ import { ACCOUNT_LIMITS } from "./constants.js" ;
45import { CodexAuthError } from "./errors.js" ;
56import type { CooldownReason } from "./storage.js" ;
67import type { TokenResult } from "./types.js" ;
@@ -25,80 +26,7 @@ export interface RefreshGuardianStats {
2526}
2627
2728const 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
10331export 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