Skip to content

Commit 83c2368

Browse files
daniel-lxsPrasangAPrajapati
authored andcommitted
Add exponential backoff for mid-stream retry failures (RooCodeInc#8888)
* Add exponential backoff for mid-stream retry failures - Extend StackItem with retryAttempt counter - Extract shared backoffAndAnnounce helper for consistent retry UX - Apply exponential backoff to mid-stream failures when auto-approval enabled - Add debug throw for testing mid-stream retry path * Add abort check in retry countdown loop Allows early exit from exponential backoff if task is cancelled during delay
1 parent 0bd4d7a commit 83c2368

File tree

1 file changed

+89
-41
lines changed

1 file changed

+89
-41
lines changed

src/core/task/Task.ts

Lines changed: 89 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)