Skip to content

Commit 8d48fcc

Browse files
authored
Merge branch 'RooCodeInc:main' into feat/finer-grained-control-gemini
2 parents ae0a3b7 + a92ee56 commit 8d48fcc

39 files changed

+617
-124
lines changed

packages/types/src/provider-settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,18 @@ export type ProviderSettingsEntry = z.infer<typeof providerSettingsEntrySchema>
5353
* ProviderSettings
5454
*/
5555

56+
/**
57+
* Default value for consecutive mistake limit
58+
*/
59+
export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3
60+
5661
const baseProviderSettingsSchema = z.object({
5762
includeMaxTokens: z.boolean().optional(),
5863
diffEnabled: z.boolean().optional(),
5964
fuzzyMatchThreshold: z.number().optional(),
6065
modelTemperature: z.number().nullish(),
6166
rateLimitSeconds: z.number().optional(),
67+
consecutiveMistakeLimit: z.number().min(0).optional(),
6268

6369
// Model reasoning.
6470
enableReasoningEffort: z.boolean().optional(),

src/core/config/ProviderSettingsManager.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
type ProviderSettingsEntry,
66
providerSettingsSchema,
77
providerSettingsSchemaDiscriminated,
8+
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
89
} from "@roo-code/types"
910
import { TelemetryService } from "@roo-code/telemetry"
1011

@@ -26,6 +27,7 @@ export const providerProfilesSchema = z.object({
2627
rateLimitSecondsMigrated: z.boolean().optional(),
2728
diffSettingsMigrated: z.boolean().optional(),
2829
openAiHeadersMigrated: z.boolean().optional(),
30+
consecutiveMistakeLimitMigrated: z.boolean().optional(),
2931
})
3032
.optional(),
3133
})
@@ -48,6 +50,7 @@ export class ProviderSettingsManager {
4850
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
4951
diffSettingsMigrated: true, // Mark as migrated on fresh installs
5052
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
53+
consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs
5154
},
5255
}
5356

@@ -113,6 +116,7 @@ export class ProviderSettingsManager {
113116
rateLimitSecondsMigrated: false,
114117
diffSettingsMigrated: false,
115118
openAiHeadersMigrated: false,
119+
consecutiveMistakeLimitMigrated: false,
116120
} // Initialize with default values
117121
isDirty = true
118122
}
@@ -135,6 +139,12 @@ export class ProviderSettingsManager {
135139
isDirty = true
136140
}
137141

142+
if (!providerProfiles.migrations.consecutiveMistakeLimitMigrated) {
143+
await this.migrateConsecutiveMistakeLimit(providerProfiles)
144+
providerProfiles.migrations.consecutiveMistakeLimitMigrated = true
145+
isDirty = true
146+
}
147+
138148
if (isDirty) {
139149
await this.store(providerProfiles)
140150
}
@@ -228,6 +238,18 @@ export class ProviderSettingsManager {
228238
}
229239
}
230240

241+
private async migrateConsecutiveMistakeLimit(providerProfiles: ProviderProfiles) {
242+
try {
243+
for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
244+
if (apiConfig.consecutiveMistakeLimit == null) {
245+
apiConfig.consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
246+
}
247+
}
248+
} catch (error) {
249+
console.error(`[MigrateConsecutiveMistakeLimit] Failed to migrate consecutive mistake limit:`, error)
250+
}
251+
}
252+
231253
/**
232254
* List all available configs with metadata.
233255
*/

src/core/config/__tests__/ProviderSettingsManager.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ describe("ProviderSettingsManager", () => {
6666
rateLimitSecondsMigrated: true,
6767
diffSettingsMigrated: true,
6868
openAiHeadersMigrated: true,
69+
consecutiveMistakeLimitMigrated: true,
6970
},
7071
}),
7172
)
@@ -144,6 +145,47 @@ describe("ProviderSettingsManager", () => {
144145
expect(storedConfig.apiConfigs.existing.rateLimitSeconds).toEqual(43)
145146
})
146147

148+
it("should call migrateConsecutiveMistakeLimit if it has not done so already", async () => {
149+
mockSecrets.get.mockResolvedValue(
150+
JSON.stringify({
151+
currentApiConfigName: "default",
152+
apiConfigs: {
153+
default: {
154+
config: {},
155+
id: "default",
156+
consecutiveMistakeLimit: undefined,
157+
},
158+
test: {
159+
apiProvider: "anthropic",
160+
consecutiveMistakeLimit: undefined,
161+
},
162+
existing: {
163+
apiProvider: "anthropic",
164+
// this should not really be possible, unless someone has loaded a hand edited config,
165+
// but we don't overwrite so we'll check that
166+
consecutiveMistakeLimit: 5,
167+
},
168+
},
169+
migrations: {
170+
rateLimitSecondsMigrated: true,
171+
diffSettingsMigrated: true,
172+
openAiHeadersMigrated: true,
173+
consecutiveMistakeLimitMigrated: false,
174+
},
175+
}),
176+
)
177+
178+
await providerSettingsManager.initialize()
179+
180+
// Get the last call to store, which should contain the migrated config
181+
const calls = mockSecrets.store.mock.calls
182+
const storedConfig = JSON.parse(calls[calls.length - 1][1])
183+
expect(storedConfig.apiConfigs.default.consecutiveMistakeLimit).toEqual(3)
184+
expect(storedConfig.apiConfigs.test.consecutiveMistakeLimit).toEqual(3)
185+
expect(storedConfig.apiConfigs.existing.consecutiveMistakeLimit).toEqual(5)
186+
expect(storedConfig.migrations.consecutiveMistakeLimitMigrated).toEqual(true)
187+
})
188+
147189
it("should throw error if secrets storage fails", async () => {
148190
mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
149191

src/core/environment/getEnvironmentDetails.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -179,22 +179,12 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
179179
// Add current time information with timezone.
180180
const now = new Date()
181181

182-
const formatter = new Intl.DateTimeFormat(undefined, {
183-
year: "numeric",
184-
month: "numeric",
185-
day: "numeric",
186-
hour: "numeric",
187-
minute: "numeric",
188-
second: "numeric",
189-
hour12: true,
190-
})
191-
192-
const timeZone = formatter.resolvedOptions().timeZone
182+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
193183
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
194184
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
195185
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
196186
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
197-
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
187+
details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}`
198188

199189
// Add context tokens information.
200190
const { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)

src/core/task/Task.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type ClineMessage,
1919
type ClineSay,
2020
type ToolProgressStatus,
21+
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
2122
type HistoryItem,
2223
TelemetryEventName,
2324
TodoItem,
@@ -216,7 +217,7 @@ export class Task extends EventEmitter<ClineEvents> {
216217
enableDiff = false,
217218
enableCheckpoints = true,
218219
fuzzyMatchThreshold = 1.0,
219-
consecutiveMistakeLimit = 3,
220+
consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
220221
task,
221222
images,
222223
historyItem,
@@ -255,7 +256,7 @@ export class Task extends EventEmitter<ClineEvents> {
255256
this.browserSession = new BrowserSession(provider.context)
256257
this.diffEnabled = enableDiff
257258
this.fuzzyMatchThreshold = fuzzyMatchThreshold
258-
this.consecutiveMistakeLimit = consecutiveMistakeLimit
259+
this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
259260
this.providerRef = new WeakRef(provider)
260261
this.globalStoragePath = provider.context.globalStorageUri.fsPath
261262
this.diffViewProvider = new DiffViewProvider(this.cwd)
@@ -1159,7 +1160,7 @@ export class Task extends EventEmitter<ClineEvents> {
11591160
throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`)
11601161
}
11611162

1162-
if (this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) {
1163+
if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) {
11631164
const { response, text, images } = await this.ask(
11641165
"mistake_limit_reached",
11651166
t("common:errors.mistake_limit_guidance"),

src/core/task/__tests__/Task.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,70 @@ describe("Cline", () => {
320320
expect(cline.diffStrategy).toBeDefined()
321321
})
322322

323+
it("should use default consecutiveMistakeLimit when not provided", () => {
324+
const cline = new Task({
325+
provider: mockProvider,
326+
apiConfiguration: mockApiConfig,
327+
task: "test task",
328+
startTask: false,
329+
})
330+
331+
expect(cline.consecutiveMistakeLimit).toBe(3)
332+
})
333+
334+
it("should respect provided consecutiveMistakeLimit", () => {
335+
const cline = new Task({
336+
provider: mockProvider,
337+
apiConfiguration: mockApiConfig,
338+
consecutiveMistakeLimit: 5,
339+
task: "test task",
340+
startTask: false,
341+
})
342+
343+
expect(cline.consecutiveMistakeLimit).toBe(5)
344+
})
345+
346+
it("should keep consecutiveMistakeLimit of 0 as 0 for unlimited", () => {
347+
const cline = new Task({
348+
provider: mockProvider,
349+
apiConfiguration: mockApiConfig,
350+
consecutiveMistakeLimit: 0,
351+
task: "test task",
352+
startTask: false,
353+
})
354+
355+
expect(cline.consecutiveMistakeLimit).toBe(0)
356+
})
357+
358+
it("should pass 0 to ToolRepetitionDetector for unlimited mode", () => {
359+
const cline = new Task({
360+
provider: mockProvider,
361+
apiConfiguration: mockApiConfig,
362+
consecutiveMistakeLimit: 0,
363+
task: "test task",
364+
startTask: false,
365+
})
366+
367+
// The toolRepetitionDetector should be initialized with 0 for unlimited mode
368+
expect(cline.toolRepetitionDetector).toBeDefined()
369+
// Verify the limit remains as 0
370+
expect(cline.consecutiveMistakeLimit).toBe(0)
371+
})
372+
373+
it("should pass consecutiveMistakeLimit to ToolRepetitionDetector", () => {
374+
const cline = new Task({
375+
provider: mockProvider,
376+
apiConfiguration: mockApiConfig,
377+
consecutiveMistakeLimit: 5,
378+
task: "test task",
379+
startTask: false,
380+
})
381+
382+
// The toolRepetitionDetector should be initialized with the same limit
383+
expect(cline.toolRepetitionDetector).toBeDefined()
384+
expect(cline.consecutiveMistakeLimit).toBe(5)
385+
})
386+
323387
it("should require either task or historyItem", () => {
324388
expect(() => {
325389
new Task({ provider: mockProvider, apiConfiguration: mockApiConfig })

src/core/tools/ToolRepetitionDetector.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ export class ToolRepetitionDetector {
4343
this.previousToolCallJson = currentToolCallJson
4444
}
4545

46-
// Check if limit is reached
47-
if (this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit) {
46+
// Check if limit is reached (0 means unlimited)
47+
if (
48+
this.consecutiveIdenticalToolCallLimit > 0 &&
49+
this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit
50+
) {
4851
// Reset counters to allow recovery if user guides the AI past this point
4952
this.consecutiveIdenticalToolCallCount = 0
5053
this.previousToolCallJson = null

src/core/tools/__tests__/ToolRepetitionDetector.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,5 +301,61 @@ describe("ToolRepetitionDetector", () => {
301301
expect(result3.allowExecution).toBe(false)
302302
expect(result3.askUser).toBeDefined()
303303
})
304+
305+
it("should never block when limit is 0 (unlimited)", () => {
306+
const detector = new ToolRepetitionDetector(0)
307+
308+
// Try many identical calls
309+
for (let i = 0; i < 10; i++) {
310+
const result = detector.check(createToolUse("tool", "tool-name"))
311+
expect(result.allowExecution).toBe(true)
312+
expect(result.askUser).toBeUndefined()
313+
}
314+
})
315+
316+
it("should handle different limits correctly", () => {
317+
// Test with limit of 5
318+
const detector5 = new ToolRepetitionDetector(5)
319+
const tool = createToolUse("tool", "tool-name")
320+
321+
// First 4 calls should be allowed
322+
for (let i = 0; i < 4; i++) {
323+
const result = detector5.check(tool)
324+
expect(result.allowExecution).toBe(true)
325+
expect(result.askUser).toBeUndefined()
326+
}
327+
328+
// 5th call should be blocked
329+
const result5 = detector5.check(tool)
330+
expect(result5.allowExecution).toBe(false)
331+
expect(result5.askUser).toBeDefined()
332+
expect(result5.askUser?.messageKey).toBe("mistake_limit_reached")
333+
})
334+
335+
it("should reset counter after blocking and allow new attempts", () => {
336+
const detector = new ToolRepetitionDetector(2)
337+
const tool = createToolUse("tool", "tool-name")
338+
339+
// First call allowed
340+
expect(detector.check(tool).allowExecution).toBe(true)
341+
342+
// Second call should block (limit is 2)
343+
const blocked = detector.check(tool)
344+
expect(blocked.allowExecution).toBe(false)
345+
346+
// After blocking, counter should reset and allow new attempts
347+
expect(detector.check(tool).allowExecution).toBe(true)
348+
})
349+
350+
it("should handle negative limits as 0 (unlimited)", () => {
351+
const detector = new ToolRepetitionDetector(-1)
352+
353+
// Should behave like unlimited
354+
for (let i = 0; i < 5; i++) {
355+
const result = detector.check(createToolUse("tool", "tool-name"))
356+
expect(result.allowExecution).toBe(true)
357+
expect(result.askUser).toBeUndefined()
358+
}
359+
})
304360
})
305361
})

src/core/webview/ClineProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ export class ClineProvider
553553
enableDiff,
554554
enableCheckpoints,
555555
fuzzyMatchThreshold,
556+
consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
556557
task,
557558
images,
558559
experiments,
@@ -589,6 +590,7 @@ export class ClineProvider
589590
enableDiff,
590591
enableCheckpoints,
591592
fuzzyMatchThreshold,
593+
consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit,
592594
historyItem,
593595
experiments,
594596
rootTask: historyItem.rootTask,

src/services/checkpoints/excludes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const getBuildArtifactPatterns = () => [
1111
".next/",
1212
".nuxt/",
1313
".sass-cache/",
14+
".terraform/",
15+
".terragrunt-cache/",
1416
".vs/",
1517
".vscode/",
1618
"Pods/",

0 commit comments

Comments
 (0)