Skip to content

Commit e10004d

Browse files
committed
Use an exponential delay for API retries
1 parent b543bd9 commit e10004d

File tree

3 files changed

+38
-29
lines changed

3 files changed

+38
-29
lines changed

.changeset/tame-walls-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Use an exponential backoff for API retries

src/core/Cline.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ export class Cline {
793793
}
794794
}
795795

796-
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
796+
async *attemptApiRequest(previousApiReqIndex: number, retryAttempt: number = 0): ApiStream {
797797
let mcpHub: McpHub | undefined
798798

799799
const { mcpEnabled, alwaysApproveResubmit, requestDelaySeconds } =
@@ -887,21 +887,29 @@ export class Cline {
887887
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
888888
if (alwaysApproveResubmit) {
889889
const errorMsg = error.message ?? "Unknown error"
890-
const requestDelay = requestDelaySeconds || 5
891-
// Automatically retry with delay
892-
// Show countdown timer in error color
893-
for (let i = requestDelay; i > 0; i--) {
890+
const baseDelay = requestDelaySeconds || 5
891+
const exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
892+
893+
// Show countdown timer with exponential backoff
894+
for (let i = exponentialDelay; i > 0; i--) {
894895
await this.say(
895896
"api_req_retry_delayed",
896-
`${errorMsg}\n\nRetrying in ${i} seconds...`,
897+
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying in ${i} seconds...`,
897898
undefined,
898899
true,
899900
)
900901
await delay(1000)
901902
}
902-
await this.say("api_req_retry_delayed", `${errorMsg}\n\nRetrying now...`, undefined, false)
903-
// delegate generator output from the recursive call
904-
yield* this.attemptApiRequest(previousApiReqIndex)
903+
904+
await this.say(
905+
"api_req_retry_delayed",
906+
`${errorMsg}\n\nRetry attempt ${retryAttempt + 1}\nRetrying now...`,
907+
undefined,
908+
false,
909+
)
910+
911+
// delegate generator output from the recursive call with incremented retry count
912+
yield* this.attemptApiRequest(previousApiReqIndex, retryAttempt + 1)
905913
return
906914
} else {
907915
const { response } = await this.ask(

src/core/__tests__/Cline.test.ts

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -730,25 +730,19 @@ describe("Cline", () => {
730730
const iterator = cline.attemptApiRequest(0)
731731
await iterator.next()
732732

733+
// Calculate expected delay for first retry
734+
const baseDelay = 3 // from requestDelaySeconds
735+
733736
// Verify countdown messages
734-
expect(saySpy).toHaveBeenCalledWith(
735-
"api_req_retry_delayed",
736-
expect.stringContaining("Retrying in 3 seconds"),
737-
undefined,
738-
true,
739-
)
740-
expect(saySpy).toHaveBeenCalledWith(
741-
"api_req_retry_delayed",
742-
expect.stringContaining("Retrying in 2 seconds"),
743-
undefined,
744-
true,
745-
)
746-
expect(saySpy).toHaveBeenCalledWith(
747-
"api_req_retry_delayed",
748-
expect.stringContaining("Retrying in 1 seconds"),
749-
undefined,
750-
true,
751-
)
737+
for (let i = baseDelay; i > 0; i--) {
738+
expect(saySpy).toHaveBeenCalledWith(
739+
"api_req_retry_delayed",
740+
expect.stringContaining(`Retrying in ${i} seconds`),
741+
undefined,
742+
true,
743+
)
744+
}
745+
752746
expect(saySpy).toHaveBeenCalledWith(
753747
"api_req_retry_delayed",
754748
expect.stringContaining("Retrying now"),
@@ -757,12 +751,14 @@ describe("Cline", () => {
757751
)
758752

759753
// Verify delay was called correctly
760-
expect(mockDelay).toHaveBeenCalledTimes(3)
754+
expect(mockDelay).toHaveBeenCalledTimes(baseDelay)
761755
expect(mockDelay).toHaveBeenCalledWith(1000)
762756

763757
// Verify error message content
764758
const errorMessage = saySpy.mock.calls.find((call) => call[1]?.includes(mockError.message))?.[1]
765-
expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`)
759+
expect(errorMessage).toBe(
760+
`${mockError.message}\n\nRetry attempt 1\nRetrying in ${baseDelay} seconds...`,
761+
)
766762
})
767763

768764
describe("loadContext", () => {

0 commit comments

Comments
 (0)