Skip to content
8 changes: 8 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export const globalSettingsSchema = z.object({
terminalZdotdir: z.boolean().optional(),
terminalCompressProgressBar: z.boolean().optional(),

// Timeout settings
toolExecutionTimeoutMs: z.number().min(1000).max(1800000).optional(), // 1s to 30min
timeoutFallbackEnabled: z.boolean().optional(),

rateLimitSeconds: z.number().optional(),
diffEnabled: z.boolean().optional(),
fuzzyMatchThreshold: z.number().optional(),
Expand Down Expand Up @@ -226,6 +230,10 @@ export const EVALS_SETTINGS: RooCodeSettings = {

enableCheckpoints: false,

// Timeout settings
toolExecutionTimeoutMs: 60000, // 1 minute default
timeoutFallbackEnabled: false,

rateLimitSeconds: 0,
maxOpenTabsContext: 20,
maxWorkspaceFiles: 200,
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export type ClineAsk = z.infer<typeof clineAskSchema>
* - `condense_context`: Context condensation/summarization has started
* - `condense_context_error`: Error occurred during context condensation
* - `codebase_search_result`: Results from searching the codebase
* - `tool_timeout`: Indicates a tool operation has timed out
*/
export const clineSays = [
"error",
Expand Down Expand Up @@ -107,6 +108,7 @@ export const clineSays = [
"condense_context_error",
"codebase_search_result",
"user_edit_todos",
"tool_timeout",
] as const

export const clineSaySchema = z.enum(clineSays)
Expand Down
135 changes: 135 additions & 0 deletions src/core/prompts/__tests__/timeout-fallback-responses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, it, expect } from "vitest"
import { formatResponse } from "../responses"

describe("timeout fallback responses", () => {
describe("generateContextualSuggestions", () => {
it("should generate execute_command suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions(
"execute_command",
{ command: "npm install" },
)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("npm install")
expect(suggestions[0].text).toContain("smaller, sequential steps")
})

it("should generate read_file suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions("read_file", {
path: "/large/file.txt",
})

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("/large/file.txt")
expect(suggestions[0].text).toContain("smaller chunks")
})

it("should generate write_to_file suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions(
"write_to_file",
{ path: "/output/file.js" },
)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("/output/file.js")
expect(suggestions[0].text).toContain("insert_content")
})

it("should generate browser_action suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions(
"browser_action",
{ action: "click" },
)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("click")
expect(suggestions[0].text).toContain("smaller, more targeted steps")
})

it("should generate search_files suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions(
"search_files",
{ regex: "complex.*pattern" },
)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("Narrow the search scope")
})

it("should generate generic suggestions for unknown tools", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateContextualSuggestions(
"unknown_tool" as any,
)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("unknown_tool operation")
expect(suggestions[0].text).toContain("smaller steps")
})
})

describe("individual suggestion generators", () => {
it("should generate command suggestions with default command name", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateCommandSuggestions()

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("the command")
})

it("should generate read file suggestions with default file name", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateReadFileSuggestions()

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("the file")
})

it("should generate write file suggestions with default file name", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateWriteFileSuggestions()

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("the file")
})

it("should generate browser suggestions with default action name", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateBrowserSuggestions()

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("browser action")
})

it("should generate search suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateSearchSuggestions()

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("Narrow the search scope")
})

it("should generate generic suggestions", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task")

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toContain("new_task operation")
})
})

describe("suggestion structure", () => {
it("should return suggestions with text property", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task")

suggestions.forEach((suggestion) => {
expect(suggestion).toHaveProperty("text")
expect(typeof suggestion.text).toBe("string")
expect(suggestion.text.length).toBeGreaterThan(0)
})
})

it("should optionally include mode property", () => {
const suggestions = formatResponse.timeoutFallbackSuggestions.generateGenericSuggestions("new_task")

suggestions.forEach((suggestion) => {
if (suggestion.mode) {
expect(typeof suggestion.mode).toBe("string")
}
})
})
})
})
134 changes: 134 additions & 0 deletions src/core/prompts/instructions/__tests__/timeout-fallback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { describe, it, expect } from "vitest"
import {
createTimeoutFallbackPrompt,
parseTimeoutFallbackResponse,
type TimeoutFallbackContext,
} from "../timeout-fallback"

describe("timeout-fallback", () => {
describe("createTimeoutFallbackPrompt", () => {
it("should create a basic prompt with required context", () => {
const context: TimeoutFallbackContext = {
toolName: "execute_command",
timeoutMs: 30000,
executionTimeMs: 32000,
}

const prompt = createTimeoutFallbackPrompt(context)

expect(prompt).toContain("execute_command operation has timed out after 30 seconds")
expect(prompt).toContain("actual execution time: 32 seconds")
expect(prompt).toContain("Tool: execute_command")
expect(prompt).toContain("Generate exactly 3-4 specific, actionable suggestions")
})

it("should include tool parameters when provided", () => {
const context: TimeoutFallbackContext = {
toolName: "read_file",
timeoutMs: 15000,
executionTimeMs: 16000,
toolParams: {
path: "/large/file.txt",
line_range: "1-10000",
},
}

const prompt = createTimeoutFallbackPrompt(context)

expect(prompt).toContain("Parameters:")
expect(prompt).toContain("/large/file.txt")
expect(prompt).toContain("1-10000")
})

it("should include task context when provided", () => {
const context: TimeoutFallbackContext = {
toolName: "write_to_file",
timeoutMs: 20000,
executionTimeMs: 22000,
taskContext: {
currentStep: "Creating configuration file",
workingDirectory: "/project/config",
},
}

const prompt = createTimeoutFallbackPrompt(context)

expect(prompt).toContain("Current step: Creating configuration file")
expect(prompt).toContain("Working directory: /project/config")
})
})

describe("parseTimeoutFallbackResponse", () => {
it("should parse numbered list responses", () => {
const response = `Here are the suggestions:
1. Break the command into smaller parts
2. Use background execution with nohup
3. Try an alternative approach
4. Increase the timeout setting`

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toBe("Break the command into smaller parts")
expect(suggestions[1].text).toBe("Use background execution with nohup")
expect(suggestions[2].text).toBe("Try an alternative approach")
expect(suggestions[3].text).toBe("Increase the timeout setting")
})

it("should parse numbered list with parentheses", () => {
const response = `Suggestions:
1) Check file permissions
2) Use smaller chunks
3) Try a different tool`

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions).toHaveLength(3)
expect(suggestions[0].text).toBe("Check file permissions")
expect(suggestions[1].text).toBe("Use smaller chunks")
expect(suggestions[2].text).toBe("Try a different tool")
})

it("should fallback to sentence parsing when no numbered list found", () => {
const response = `You should try breaking the operation into smaller parts. Consider using an alternative approach. Check system resources and try again.`

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions.length).toBeGreaterThan(0)
expect(suggestions[0].text).toBe("You should try breaking the operation into smaller parts")
})

it("should limit suggestions to 4 items", () => {
const response = `1. First suggestion
2. Second suggestion
3. Third suggestion
4. Fourth suggestion
5. Fifth suggestion
6. Sixth suggestion`

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions).toHaveLength(4)
})

it("should filter out suggestions that are too long", () => {
const response = `1. Good suggestion
2. This is a very long suggestion that exceeds the maximum character limit and should be filtered out because it's too verbose
3. Another good suggestion`

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions).toHaveLength(2)
expect(suggestions[0].text).toBe("Good suggestion")
expect(suggestions[1].text).toBe("Another good suggestion")
})

it("should return empty array for invalid responses", () => {
const response = ""

const suggestions = parseTimeoutFallbackResponse(response)

expect(suggestions).toHaveLength(0)
})
})
})
Loading