feat: Add configurable min/max retry delay bounds

- Add minRetryDelaySeconds and maxRetryDelaySeconds settings to global schema
- Replace single retry delay slider with separate min/max controls in UI
- Implement bounded exponential backoff in Task.ts retry logic
- Add translation keys for new settings labels and descriptions
- Update all type definitions and message handlers
- Fix test file to include new required properties

Fixes #5189: Prevents excessive retry delays (20+ minutes) by allowing users to configure minimum (1-60s) and maximum (10-300s) delay bounds for exponential backoff retry behavior.
This commit is contained in:
Roo Code 2025-06-30 16:56:45 +00:00
parent 3a8ba27615
commit 60d903d3a5
10 changed files with 84 additions and 13 deletions

View file

@ -41,6 +41,8 @@ export const globalSettingsSchema = z.object({
alwaysAllowBrowser: z.boolean().optional(),
alwaysApproveResubmit: z.boolean().optional(),
requestDelaySeconds: z.number().optional(),
minRetryDelaySeconds: z.number().optional(),
maxRetryDelaySeconds: z.number().optional(),
alwaysAllowMcp: z.boolean().optional(),
alwaysAllowModeSwitch: z.boolean().optional(),
alwaysAllowSubtasks: z.boolean().optional(),

View file

@ -1640,6 +1640,8 @@ export class Task extends EventEmitter<ClineEvents> {
autoApprovalEnabled,
alwaysApproveResubmit,
requestDelaySeconds,
minRetryDelaySeconds,
maxRetryDelaySeconds,
mode,
autoCondenseContext = true,
autoCondenseContextPercent = 100,
@ -1794,6 +1796,11 @@ export class Task extends EventEmitter<ClineEvents> {
const baseDelay = requestDelaySeconds || 5
let exponentialDelay = Math.ceil(baseDelay * Math.pow(2, retryAttempt))
// Apply min/max bounds to the exponential delay
const minDelay = minRetryDelaySeconds || 5
const maxDelay = maxRetryDelaySeconds || 100
exponentialDelay = Math.min(Math.max(exponentialDelay, minDelay), maxDelay)
// If the error is a 429, and the error details contain a retry delay, use that delay instead of exponential backoff
if (error.status === 429) {
const geminiRetryDetails = error.errorDetails?.find(

View file

@ -861,6 +861,14 @@ export const webviewMessageHandler = async (
await updateGlobalState("requestDelaySeconds", message.value ?? 5)
await provider.postStateToWebview()
break
case "minRetryDelaySeconds":
await updateGlobalState("minRetryDelaySeconds", message.value ?? 5)
await provider.postStateToWebview()
break
case "maxRetryDelaySeconds":
await updateGlobalState("maxRetryDelaySeconds", message.value ?? 100)
await provider.postStateToWebview()
break
case "writeDelayMs":
await updateGlobalState("writeDelayMs", message.value)
await provider.postStateToWebview()

View file

@ -229,6 +229,8 @@ export type ExtensionState = Pick<
writeDelayMs: number
requestDelaySeconds: number
minRetryDelaySeconds: number
maxRetryDelaySeconds: number
enableCheckpoints: boolean
maxOpenTabsContext: number // Maximum number of VSCode open tabs to include in context (0-500)

View file

@ -116,6 +116,8 @@ export interface WebviewMessage {
| "searchCommits"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "minRetryDelaySeconds"
| "maxRetryDelaySeconds"
| "setApiConfigPassword"
| "mode"
| "updatePrompt"

View file

@ -21,6 +21,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
alwaysAllowBrowser?: boolean
alwaysApproveResubmit?: boolean
requestDelaySeconds: number
minRetryDelaySeconds: number
maxRetryDelaySeconds: number
alwaysAllowMcp?: boolean
alwaysAllowModeSwitch?: boolean
alwaysAllowSubtasks?: boolean
@ -36,6 +38,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "alwaysAllowBrowser"
| "alwaysApproveResubmit"
| "requestDelaySeconds"
| "minRetryDelaySeconds"
| "maxRetryDelaySeconds"
| "alwaysAllowMcp"
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
@ -54,6 +58,8 @@ export const AutoApproveSettings = ({
alwaysAllowBrowser,
alwaysApproveResubmit,
requestDelaySeconds,
minRetryDelaySeconds,
maxRetryDelaySeconds,
alwaysAllowMcp,
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
@ -184,19 +190,43 @@ export const AutoApproveSettings = ({
<div>{t("settings:autoApprove.retry.label")}</div>
</div>
<div>
<div className="flex items-center gap-2">
<Slider
min={5}
max={100}
step={1}
value={[requestDelaySeconds]}
onValueChange={([value]) => setCachedStateField("requestDelaySeconds", value)}
data-testid="request-delay-slider"
/>
<span className="w-20">{requestDelaySeconds}s</span>
<div className="mb-3">
<label className="block font-medium mb-1">
{t("settings:autoApprove.retry.minDelayLabel")}
</label>
<div className="flex items-center gap-2">
<Slider
min={1}
max={60}
step={1}
value={[minRetryDelaySeconds]}
onValueChange={([value]) => setCachedStateField("minRetryDelaySeconds", value)}
data-testid="min-retry-delay-slider"
/>
<span className="w-20">{minRetryDelaySeconds}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.retry.minDelayDescription")}
</div>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.retry.delayLabel")}
<div>
<label className="block font-medium mb-1">
{t("settings:autoApprove.retry.maxDelayLabel")}
</label>
<div className="flex items-center gap-2">
<Slider
min={10}
max={300}
step={5}
value={[maxRetryDelaySeconds]}
onValueChange={([value]) => setCachedStateField("maxRetryDelaySeconds", value)}
data-testid="max-retry-delay-slider"
/>
<span className="w-20">{maxRetryDelaySeconds}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.retry.maxDelayDescription")}
</div>
</div>
</div>
</div>

View file

@ -146,6 +146,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
maxWorkspaceFiles,
mcpEnabled,
requestDelaySeconds,
minRetryDelaySeconds,
maxRetryDelaySeconds,
remoteBrowserHost,
screenshotQuality,
soundEnabled,
@ -302,6 +304,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "mcpEnabled", bool: mcpEnabled })
vscode.postMessage({ type: "alwaysApproveResubmit", bool: alwaysApproveResubmit })
vscode.postMessage({ type: "requestDelaySeconds", value: requestDelaySeconds })
vscode.postMessage({ type: "minRetryDelaySeconds", value: minRetryDelaySeconds })
vscode.postMessage({ type: "maxRetryDelaySeconds", value: maxRetryDelaySeconds })
vscode.postMessage({ type: "maxOpenTabsContext", value: maxOpenTabsContext })
vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 })
vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles })
@ -595,6 +599,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowBrowser={alwaysAllowBrowser}
alwaysApproveResubmit={alwaysApproveResubmit}
requestDelaySeconds={requestDelaySeconds}
minRetryDelaySeconds={minRetryDelaySeconds}
maxRetryDelaySeconds={maxRetryDelaySeconds}
alwaysAllowMcp={alwaysAllowMcp}
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}

View file

@ -91,6 +91,10 @@ export interface ExtensionStateContextType extends ExtensionState {
setAlwaysApproveResubmit: (value: boolean) => void
requestDelaySeconds: number
setRequestDelaySeconds: (value: number) => void
minRetryDelaySeconds: number
setMinRetryDelaySeconds: (value: number) => void
maxRetryDelaySeconds: number
setMaxRetryDelaySeconds: (value: number) => void
setCurrentApiConfigName: (value: string) => void
setListApiConfigMeta: (value: ProviderSettingsEntry[]) => void
mode: Mode
@ -173,6 +177,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
enableMcpServerCreation: false,
alwaysApproveResubmit: false,
requestDelaySeconds: 5,
minRetryDelaySeconds: 5,
maxRetryDelaySeconds: 100,
currentApiConfigName: "default",
listApiConfigMeta: [],
mode: defaultModeSlug,
@ -393,6 +399,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
setMinRetryDelaySeconds: (value) => setState((prevState) => ({ ...prevState, minRetryDelaySeconds: value })),
setMaxRetryDelaySeconds: (value) => setState((prevState) => ({ ...prevState, maxRetryDelaySeconds: value })),
setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
setListApiConfigMeta,
setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),

View file

@ -191,6 +191,8 @@ describe("mergeExtensionState", () => {
enableCheckpoints: true,
writeDelayMs: 1000,
requestDelaySeconds: 5,
minRetryDelaySeconds: 5,
maxRetryDelaySeconds: 100,
mode: "default",
experiments: {} as Record<ExperimentId, boolean>,
customModes: [],

View file

@ -96,7 +96,11 @@
"retry": {
"label": "Retry",
"description": "Automatically retry failed API requests when server returns an error response",
"delayLabel": "Delay before retrying the request"
"delayLabel": "Delay before retrying the request",
"minDelayLabel": "Minimum retry delay",
"maxDelayLabel": "Maximum retry delay",
"minDelayDescription": "Minimum delay before retrying failed requests (prevents excessive rapid retries)",
"maxDelayDescription": "Maximum delay before retrying failed requests (prevents excessively long waits)"
},
"mcp": {
"label": "MCP",