@@ -1749,9 +1749,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
17491749 interface StackItem {
17501750 userContent : Anthropic . Messages . ContentBlockParam [ ]
17511751 includeFileDetails : boolean
1752+ retryAttempt ?: number
17521753 }
17531754
1754- const stack : StackItem [ ] = [ { userContent, includeFileDetails } ]
1755+ const stack : StackItem [ ] = [ { userContent, includeFileDetails, retryAttempt : 0 } ]
17551756
17561757 while ( stack . length > 0 ) {
17571758 const currentItem = stack . pop ( ) !
@@ -2231,10 +2232,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
22312232 `[Task#${ this . taskId } .${ this . instanceId } ] Stream failed, will retry: ${ streamingFailedMessage } ` ,
22322233 )
22332234
2234- // Push the same content back onto the stack to retry
2235+ // Apply exponential backoff similar to first-chunk errors when auto-resubmit is enabled
2236+ const stateForBackoff = await this . providerRef . deref ( ) ?. getState ( )
2237+ if ( stateForBackoff ?. autoApprovalEnabled && stateForBackoff ?. alwaysApproveResubmit ) {
2238+ await this . backoffAndAnnounce (
2239+ currentItem . retryAttempt ?? 0 ,
2240+ error ,
2241+ streamingFailedMessage ,
2242+ )
2243+ }
2244+
2245+ // Push the same content back onto the stack to retry, incrementing the retry attempt counter
22352246 stack . push ( {
22362247 userContent : currentUserContent ,
22372248 includeFileDetails : false ,
2249+ retryAttempt : ( currentItem . retryAttempt ?? 0 ) + 1 ,
22382250 } )
22392251
22402252 // Continue to retry the request
@@ -2775,45 +2787,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
27752787 errorMsg = "Unknown error"
27762788 }
27772789
2778- const baseDelay = requestDelaySeconds || 5
2779- let exponentialDelay = Math . min (
2780- Math . ceil ( baseDelay * Math . pow ( 2 , retryAttempt ) ) ,
2781- MAX_EXPONENTIAL_BACKOFF_SECONDS ,
2782- )
2783-
2784- // If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff
2785- if ( error . status === 429 ) {
2786- const geminiRetryDetails = error . errorDetails ?. find (
2787- ( detail : any ) => detail [ "@type" ] === "type.googleapis.com/google.rpc.RetryInfo" ,
2788- )
2789- if ( geminiRetryDetails ) {
2790- const match = geminiRetryDetails ?. retryDelay ?. match ( / ^ ( \d + ) s $ / )
2791- if ( match ) {
2792- exponentialDelay = Number ( match [ 1 ] ) + 1
2793- }
2794- }
2795- }
2796-
2797- // Wait for the greater of the exponential delay or the rate limit delay
2798- const finalDelay = Math . max ( exponentialDelay , rateLimitDelay )
2799-
2800- // Show countdown timer with exponential backoff
2801- for ( let i = finalDelay ; i > 0 ; i -- ) {
2802- await this . say (
2803- "api_req_retry_delayed" ,
2804- `${ errorMsg } \n\nRetry attempt ${ retryAttempt + 1 } \nRetrying in ${ i } seconds...` ,
2805- undefined ,
2806- true ,
2807- )
2808- await delay ( 1000 )
2809- }
2810-
2811- await this . say (
2812- "api_req_retry_delayed" ,
2813- `${ errorMsg } \n\nRetry attempt ${ retryAttempt + 1 } \nRetrying now...` ,
2814- undefined ,
2815- false ,
2816- )
2790+ // Apply shared exponential backoff and countdown UX
2791+ await this . backoffAndAnnounce ( retryAttempt , error , errorMsg )
28172792
28182793 // Delegate generator output from the recursive call with
28192794 // incremented retry count.
@@ -2851,6 +2826,79 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
28512826 yield * iterator
28522827 }
28532828
2829+ // Shared exponential backoff for retries (first-chunk and mid-stream)
2830+ private async backoffAndAnnounce ( retryAttempt : number , error : any , header ?: string ) : Promise < void > {
2831+ try {
2832+ const state = await this . providerRef . deref ( ) ?. getState ( )
2833+ const baseDelay = state ?. requestDelaySeconds || 5
2834+
2835+ let exponentialDelay = Math . min (
2836+ Math . ceil ( baseDelay * Math . pow ( 2 , retryAttempt ) ) ,
2837+ MAX_EXPONENTIAL_BACKOFF_SECONDS ,
2838+ )
2839+
2840+ // Respect provider rate limit window
2841+ let rateLimitDelay = 0
2842+ const rateLimit = state ?. apiConfiguration ?. rateLimitSeconds || 0
2843+ if ( Task . lastGlobalApiRequestTime && rateLimit > 0 ) {
2844+ const elapsed = performance . now ( ) - Task . lastGlobalApiRequestTime
2845+ rateLimitDelay = Math . ceil ( Math . min ( rateLimit , Math . max ( 0 , rateLimit * 1000 - elapsed ) / 1000 ) )
2846+ }
2847+
2848+ // Prefer RetryInfo on 429 if present
2849+ if ( error ?. status === 429 ) {
2850+ const retryInfo = error ?. errorDetails ?. find (
2851+ ( d : any ) => d [ "@type" ] === "type.googleapis.com/google.rpc.RetryInfo" ,
2852+ )
2853+ const match = retryInfo ?. retryDelay ?. match ?.( / ^ ( \d + ) s $ / )
2854+ if ( match ) {
2855+ exponentialDelay = Number ( match [ 1 ] ) + 1
2856+ }
2857+ }
2858+
2859+ const finalDelay = Math . max ( exponentialDelay , rateLimitDelay )
2860+ if ( finalDelay <= 0 ) return
2861+
2862+ // Build header text; fall back to error message if none provided
2863+ let headerText = header
2864+ if ( ! headerText ) {
2865+ if ( error ?. error ?. metadata ?. raw ) {
2866+ headerText = JSON . stringify ( error . error . metadata . raw , null , 2 )
2867+ } else if ( error ?. message ) {
2868+ headerText = error . message
2869+ } else {
2870+ headerText = "Unknown error"
2871+ }
2872+ }
2873+ headerText = headerText ? `${ headerText } \n\n` : ""
2874+
2875+ // Show countdown timer with exponential backoff
2876+ for ( let i = finalDelay ; i > 0 ; i -- ) {
2877+ // Check abort flag during countdown to allow early exit
2878+ if ( this . abort ) {
2879+ throw new Error ( `[Task#${ this . taskId } ] Aborted during retry countdown` )
2880+ }
2881+
2882+ await this . say (
2883+ "api_req_retry_delayed" ,
2884+ `${ headerText } Retry attempt ${ retryAttempt + 1 } \nRetrying in ${ i } seconds...` ,
2885+ undefined ,
2886+ true ,
2887+ )
2888+ await delay ( 1000 )
2889+ }
2890+
2891+ await this . say (
2892+ "api_req_retry_delayed" ,
2893+ `${ headerText } Retry attempt ${ retryAttempt + 1 } \nRetrying now...` ,
2894+ undefined ,
2895+ false ,
2896+ )
2897+ } catch ( err ) {
2898+ console . error ( "Exponential backoff failed:" , err )
2899+ }
2900+ }
2901+
28542902 // Checkpoints
28552903
28562904 public async checkpointSave ( force : boolean = false , suppressMessage : boolean = false ) {
0 commit comments