@@ -70,23 +70,33 @@ export async function triggerSecurityInsightsCheckForRepos(
7070 // via `!tokenInfo`, so requeue decisions have to be made here too — otherwise a rate-limit
7171 // wave would falsely fail the repo for the rest of the continueAsNew chain.
7272 let sawRateLimit = false
73+ // Per-repo attempted-tokens set. TokenRepoAccessError releases the token in "clean"
74+ // global state, so without this filter `getNextToken` could re-pick the same token
75+ // while the others are held by concurrent children — burning attempts without ever
76+ // reaching a token that might have access.
77+ const attemptedTokens = new Set < string > ( )
7378 while ( attempts < MAX_TOKEN_ATTEMPTS ) {
74- const tokenInfo = getNextToken ( tokenInfos , currentTimeMs )
79+ const tokenInfo = getNextToken ( tokenInfos , currentTimeMs , attemptedTokens )
7580 if ( ! tokenInfo ) {
76- // Only mark the repo failed if every token is permanently invalid. Rate-limited
77- // tokens recover after 1h and in-use tokens free up as concurrent tasks finish, so
78- // in both cases requeue the repo — the outer loop's hasFreeToken gate + defer path
79- // handles the wait.
81+ // No untried, usable token remains for this repo. Distinguish two cases:
82+ // - all tokens permanently invalid, OR every non-invalid token has been tried for
83+ // this repo → this repo genuinely can't be scanned, fail it
84+ // - some non-invalid, non-attempted tokens exist but are currently in-use or
85+ // rate-limited → requeue so a later iteration can retry when they free up
8086 const allPermanentlyInvalid = tokenInfos . every ( ( t ) => t . isInvalid )
81- if ( allPermanentlyInvalid ) {
82- console . error ( `All tokens permanently invalid for repo ${ repo . repoUrl } , skipping` )
87+ const anyUnattemptedNonInvalid = tokenInfos . some (
88+ ( t ) => ! t . isInvalid && ! attemptedTokens . has ( t . token ) ,
89+ )
90+ if ( allPermanentlyInvalid || ! anyUnattemptedNonInvalid ) {
91+ console . error ( `No untried tokens remain for repo ${ repo . repoUrl } , skipping` )
8392 failedRepoUrls . push ( repo . repoUrl )
8493 } else {
8594 queue . unshift ( repo )
8695 }
8796 break
8897 }
8998 const token = tokenInfo . token
99+ attemptedTokens . add ( token )
90100 tokenInfo . inUse = true
91101
92102 try {
@@ -256,10 +266,17 @@ function refreshExpiredRateLimits(tokenInfos: ITokenInfo[], nowMs: number): void
256266 }
257267}
258268
259- function getNextToken ( tokenInfos : ITokenInfo [ ] , nowMs : number ) : ITokenInfo | null {
269+ function getNextToken (
270+ tokenInfos : ITokenInfo [ ] ,
271+ nowMs : number ,
272+ excludeTokens ?: Set < string > ,
273+ ) : ITokenInfo | null {
260274 refreshExpiredRateLimits ( tokenInfos , nowMs )
261275
262- const usableTokenInfos = tokenInfos . filter ( ( t ) => ! t . inUse && ! t . isRateLimited && ! t . isInvalid )
276+ const usableTokenInfos = tokenInfos . filter (
277+ ( t ) =>
278+ ! t . inUse && ! t . isRateLimited && ! t . isInvalid && ! ( excludeTokens && excludeTokens . has ( t . token ) ) ,
279+ )
263280
264281 // sort usable tokens by last used date from oldest to newest
265282 const sortedTokenInfos = usableTokenInfos . sort ( ( a , b ) => {
0 commit comments