mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-08 22:21:23 +00:00
working first version
This commit is contained in:
parent
7645aad435
commit
c0c7878497
21 changed files with 1736 additions and 3 deletions
|
|
@ -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(),
|
||||
|
|
@ -226,6 +230,10 @@ export const EVALS_SETTINGS: RooCodeSettings = {
|
|||
|
||||
enableCheckpoints: false,
|
||||
|
||||
// Timeout settings
|
||||
toolExecutionTimeoutMs: 300000, // 5 minutes default
|
||||
timeoutFallbackEnabled: false,
|
||||
|
||||
rateLimitSeconds: 0,
|
||||
maxOpenTabsContext: 20,
|
||||
maxWorkspaceFiles: 200,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,18 @@ export const formatResponse = {
|
|||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
|
||||
toolTimeout: (toolName: string, timeoutMs: number, executionTimeMs: number) =>
|
||||
`The ${toolName} operation timed out after ${Math.round(timeoutMs / 1000)} seconds and was automatically canceled.
|
||||
|
||||
<timeout_details>
|
||||
Tool: ${toolName}
|
||||
Configured Timeout: ${Math.round(timeoutMs / 1000)}s
|
||||
Execution Time: ${Math.round(executionTimeMs / 1000)}s
|
||||
Status: Canceled
|
||||
</timeout_details>
|
||||
|
||||
The operation has been terminated to prevent system resource issues. Please consider one of the following approaches to complete your task.`,
|
||||
|
||||
rooIgnoreError: (path: string) =>
|
||||
`Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`,
|
||||
|
||||
|
|
|
|||
358
src/core/timeout/TimeoutFallbackGenerator.ts
Normal file
358
src/core/timeout/TimeoutFallbackGenerator.ts
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
import type { ToolName } from "@roo-code/types"
|
||||
import type { Task } from "../task/Task"
|
||||
import type { SingleCompletionHandler } from "../../api"
|
||||
|
||||
export interface TimeoutFallbackContext {
|
||||
toolName: ToolName
|
||||
timeoutMs: number
|
||||
executionTimeMs: number
|
||||
toolParams?: Record<string, any>
|
||||
errorMessage?: string
|
||||
taskContext?: {
|
||||
currentStep?: string
|
||||
previousActions?: string[]
|
||||
workingDirectory?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface TimeoutFallbackResult {
|
||||
success: boolean
|
||||
toolCall?: {
|
||||
name: "ask_followup_question"
|
||||
params: {
|
||||
question: string
|
||||
follow_up: string
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates AI-powered fallback suggestions for timeout scenarios
|
||||
*/
|
||||
export class TimeoutFallbackGenerator {
|
||||
/**
|
||||
* Generate an AI-powered ask_followup_question tool call for timeout scenarios
|
||||
*/
|
||||
public static async generateAiFallback(
|
||||
context: TimeoutFallbackContext,
|
||||
task?: Task,
|
||||
): Promise<TimeoutFallbackResult> {
|
||||
// Try to use AI to generate contextual suggestions
|
||||
if (task?.api && "completePrompt" in task.api) {
|
||||
try {
|
||||
const aiResult = await this.generateAiSuggestions(context, task.api as SingleCompletionHandler)
|
||||
if (aiResult.success) {
|
||||
return aiResult
|
||||
}
|
||||
} catch (error) {
|
||||
// AI failed, fall through to static suggestions
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to static suggestions if AI fails or is unavailable
|
||||
const toolCall = this.generateStaticToolCall(context)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
toolCall,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate AI-powered suggestions using the task's API handler
|
||||
*/
|
||||
private static async generateAiSuggestions(
|
||||
context: TimeoutFallbackContext,
|
||||
apiHandler: SingleCompletionHandler,
|
||||
): Promise<TimeoutFallbackResult> {
|
||||
try {
|
||||
const prompt = this.createAiPrompt(context)
|
||||
const aiResponse = await apiHandler.completePrompt(prompt)
|
||||
|
||||
// Parse the AI response to extract suggestions
|
||||
const suggestions = this.parseAiResponse(aiResponse)
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
throw new Error("No valid suggestions generated by AI")
|
||||
}
|
||||
|
||||
const question = `The ${context.toolName} operation timed out after ${Math.round(context.timeoutMs / 1000)} seconds. How would you like to proceed?`
|
||||
|
||||
const followUpXml = suggestions
|
||||
.map((suggestion) =>
|
||||
suggestion.mode
|
||||
? `<suggest mode="${suggestion.mode}">${suggestion.text}</suggest>`
|
||||
: `<suggest>${suggestion.text}</suggest>`,
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
toolCall: {
|
||||
name: "ask_followup_question",
|
||||
params: {
|
||||
question,
|
||||
follow_up: followUpXml,
|
||||
},
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error generating AI suggestions",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a prompt for the AI to generate contextual timeout fallback suggestions
|
||||
*/
|
||||
private static createAiPrompt(context: TimeoutFallbackContext): string {
|
||||
const { toolName, timeoutMs, executionTimeMs, toolParams, taskContext } = context
|
||||
|
||||
const timeoutSeconds = Math.round(timeoutMs / 1000)
|
||||
const executionSeconds = Math.round(executionTimeMs / 1000)
|
||||
|
||||
let prompt = `A ${toolName} operation has timed out after ${timeoutSeconds} seconds (actual execution time: ${executionSeconds} seconds).
|
||||
|
||||
Context:
|
||||
- Tool: ${toolName}
|
||||
- Timeout limit: ${timeoutSeconds}s
|
||||
- Actual execution time: ${executionSeconds}s`
|
||||
|
||||
// Add tool-specific context
|
||||
if (toolParams) {
|
||||
prompt += `\n- Parameters: ${JSON.stringify(toolParams, null, 2)}`
|
||||
}
|
||||
|
||||
// Add task context if available
|
||||
if (taskContext) {
|
||||
if (taskContext.currentStep) {
|
||||
prompt += `\n- Current step: ${taskContext.currentStep}`
|
||||
}
|
||||
if (taskContext.workingDirectory) {
|
||||
prompt += `\n- Working directory: ${taskContext.workingDirectory}`
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Generate exactly 3-4 specific, actionable suggestions for how to proceed after this timeout. Each suggestion should be:
|
||||
1. Contextually relevant to the specific ${toolName} operation that timed out
|
||||
2. Actionable and specific (not generic advice)
|
||||
3. Focused on solving the immediate problem
|
||||
4. Ordered by likelihood of success
|
||||
|
||||
Format your response as a simple numbered list:
|
||||
1. [First suggestion]
|
||||
2. [Second suggestion]
|
||||
3. [Third suggestion]
|
||||
4. [Fourth suggestion (optional)]
|
||||
|
||||
Focus on practical solutions like:
|
||||
- Breaking the operation into smaller parts
|
||||
- Using alternative tools or methods
|
||||
- Adjusting parameters or settings
|
||||
- Checking for underlying issues
|
||||
- Optimizing the approach
|
||||
|
||||
Keep each suggestion concise (under 80 characters) and actionable.`
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse AI response to extract suggestions
|
||||
*/
|
||||
private static parseAiResponse(response: string): Array<{ text: string; mode?: string }> {
|
||||
const suggestions: Array<{ text: string; mode?: string }> = []
|
||||
|
||||
// Look for numbered list items
|
||||
const lines = response.split("\n")
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
|
||||
// Match patterns like "1. suggestion", "2) suggestion", etc.
|
||||
const match = trimmed.match(/^(\d+)[.)]\s*(.+)$/)
|
||||
if (match && match[2]) {
|
||||
const suggestionText = match[2].trim()
|
||||
if (suggestionText.length > 0 && suggestionText.length <= 120) {
|
||||
suggestions.push({ text: suggestionText })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no numbered list found, try to extract sentences
|
||||
if (suggestions.length === 0) {
|
||||
const sentences = response
|
||||
.split(/[.!?]+/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 10 && s.length <= 120)
|
||||
for (let i = 0; i < Math.min(4, sentences.length); i++) {
|
||||
suggestions.push({ text: sentences[i] })
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions.slice(0, 4) // Limit to 4 suggestions
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate static fallback suggestions when AI is unavailable
|
||||
*/
|
||||
private static generateStaticToolCall(context: TimeoutFallbackContext): TimeoutFallbackResult["toolCall"] {
|
||||
const suggestions = this.generateContextualSuggestions(context)
|
||||
|
||||
const question = `The ${context.toolName} operation timed out after ${Math.round(context.timeoutMs / 1000)} seconds. How would you like to proceed?`
|
||||
|
||||
const followUpXml = suggestions
|
||||
.map((suggestion) =>
|
||||
suggestion.mode
|
||||
? `<suggest mode="${suggestion.mode}">${suggestion.text}</suggest>`
|
||||
: `<suggest>${suggestion.text}</suggest>`,
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
return {
|
||||
name: "ask_followup_question",
|
||||
params: {
|
||||
question,
|
||||
follow_up: followUpXml,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate contextual suggestions based on tool type and parameters
|
||||
*/
|
||||
private static generateContextualSuggestions(
|
||||
context: TimeoutFallbackContext,
|
||||
): Array<{ text: string; mode?: string }> {
|
||||
const { toolName, toolParams } = context
|
||||
|
||||
switch (toolName) {
|
||||
case "execute_command":
|
||||
return this.generateCommandSuggestions(toolParams)
|
||||
case "read_file":
|
||||
return this.generateReadFileSuggestions(toolParams)
|
||||
case "write_to_file":
|
||||
return this.generateWriteFileSuggestions(toolParams)
|
||||
case "browser_action":
|
||||
return this.generateBrowserSuggestions(toolParams)
|
||||
case "search_files":
|
||||
return this.generateSearchSuggestions(toolParams)
|
||||
default:
|
||||
return this.generateGenericSuggestions(context)
|
||||
}
|
||||
}
|
||||
|
||||
private static generateCommandSuggestions(params?: Record<string, any>): Array<{ text: string; mode?: string }> {
|
||||
const command = params?.command || "the command"
|
||||
|
||||
return [
|
||||
{
|
||||
text: `Break "${command}" into smaller, sequential steps that can complete faster`,
|
||||
},
|
||||
{
|
||||
text: `Run "${command}" in the background using '&' or 'nohup' to avoid blocking`,
|
||||
},
|
||||
{
|
||||
text: `Try an alternative approach or tool to accomplish the same goal`,
|
||||
},
|
||||
{
|
||||
text: `Increase the timeout setting if this operation legitimately needs more time`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private static generateReadFileSuggestions(params?: Record<string, any>): Array<{ text: string; mode?: string }> {
|
||||
const filePath = params?.path || "the file"
|
||||
|
||||
return [
|
||||
{
|
||||
text: `Read "${filePath}" in smaller chunks using line ranges`,
|
||||
},
|
||||
{
|
||||
text: `Check if "${filePath}" is accessible and not locked by another process`,
|
||||
},
|
||||
{
|
||||
text: `Use a different approach to access the file content`,
|
||||
},
|
||||
{
|
||||
text: `Increase the timeout if this is a legitimately large file`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private static generateWriteFileSuggestions(params?: Record<string, any>): Array<{ text: string; mode?: string }> {
|
||||
const filePath = params?.path || "the file"
|
||||
|
||||
return [
|
||||
{
|
||||
text: `Write to "${filePath}" incrementally using insert_content instead`,
|
||||
},
|
||||
{
|
||||
text: `Check if "${filePath}" is writable and not locked`,
|
||||
},
|
||||
{
|
||||
text: `Use apply_diff for targeted changes instead of full file replacement`,
|
||||
},
|
||||
{
|
||||
text: `Break the content into smaller write operations`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private static generateBrowserSuggestions(params?: Record<string, any>): Array<{ text: string; mode?: string }> {
|
||||
const action = params?.action || "browser action"
|
||||
|
||||
return [
|
||||
{
|
||||
text: `Simplify the "${action}" into smaller, more targeted steps`,
|
||||
},
|
||||
{
|
||||
text: `Wait for specific elements to load before proceeding`,
|
||||
},
|
||||
{
|
||||
text: `Use direct API calls instead of browser automation if possible`,
|
||||
},
|
||||
{
|
||||
text: `Reset the browser session and try again`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private static generateSearchSuggestions(params?: Record<string, any>): Array<{ text: string; mode?: string }> {
|
||||
return [
|
||||
{
|
||||
text: `Narrow the search scope to specific directories`,
|
||||
},
|
||||
{
|
||||
text: `Use simpler search patterns or literal strings`,
|
||||
},
|
||||
{
|
||||
text: `Apply file type filters to reduce search space`,
|
||||
},
|
||||
{
|
||||
text: `Search incrementally in smaller batches`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
private static generateGenericSuggestions(context: TimeoutFallbackContext): Array<{ text: string; mode?: string }> {
|
||||
return [
|
||||
{
|
||||
text: `Break the ${context.toolName} operation into smaller steps`,
|
||||
},
|
||||
{
|
||||
text: `Try an alternative approach to accomplish the same goal`,
|
||||
},
|
||||
{
|
||||
text: `Check system resources and try again`,
|
||||
},
|
||||
{
|
||||
text: `Increase the timeout setting for this operation`,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
75
src/core/timeout/TimeoutFallbackHandler.ts
Normal file
75
src/core/timeout/TimeoutFallbackHandler.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import type { ToolName } from "@roo-code/types"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import {
|
||||
TimeoutFallbackGenerator,
|
||||
type TimeoutFallbackContext,
|
||||
type TimeoutFallbackResult,
|
||||
} from "./TimeoutFallbackGenerator"
|
||||
import type { Task } from "../task/Task"
|
||||
import { parseAssistantMessage } from "../assistant-message/parseAssistantMessage"
|
||||
|
||||
/**
|
||||
* Generates AI-powered fallback suggestions for timeout scenarios
|
||||
*/
|
||||
export class TimeoutFallbackHandler {
|
||||
/**
|
||||
* Create a timeout response with AI-generated fallback question
|
||||
*/
|
||||
public static async createTimeoutResponse(
|
||||
toolName: ToolName,
|
||||
timeoutMs: number,
|
||||
executionTimeMs: number,
|
||||
context?: any,
|
||||
task?: Task,
|
||||
): Promise<string> {
|
||||
const baseResponse = formatResponse.toolTimeout(toolName, timeoutMs, executionTimeMs)
|
||||
|
||||
// Create context for AI fallback generation
|
||||
const aiContext: TimeoutFallbackContext = {
|
||||
toolName,
|
||||
timeoutMs,
|
||||
executionTimeMs,
|
||||
toolParams: context,
|
||||
taskContext: task
|
||||
? {
|
||||
workingDirectory: task.cwd,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
// Generate AI-powered fallback (with static fallback if AI fails)
|
||||
const aiResult = await TimeoutFallbackGenerator.generateAiFallback(aiContext, task)
|
||||
|
||||
if (aiResult.success && aiResult.toolCall && task) {
|
||||
// Inject the tool call directly into the assistant message content for proper execution
|
||||
this.injectToolCallIntoMessageContent(aiResult.toolCall, task)
|
||||
return baseResponse
|
||||
}
|
||||
|
||||
// This should rarely happen since generateAiFallback always provides static fallback
|
||||
return `${baseResponse}\n\nThe operation timed out. Please consider breaking this into smaller steps or trying a different approach.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a tool call directly into the assistant message content for proper parsing and execution
|
||||
*/
|
||||
private static injectToolCallIntoMessageContent(toolCall: TimeoutFallbackResult["toolCall"], task: Task): void {
|
||||
if (toolCall?.name === "ask_followup_question" && toolCall.params) {
|
||||
const { question, follow_up } = toolCall.params
|
||||
|
||||
// Create the XML tool call string
|
||||
const toolCallXml = `<ask_followup_question>
|
||||
<question>${question}</question>
|
||||
<follow_up>
|
||||
${follow_up}
|
||||
</follow_up>
|
||||
</ask_followup_question>`
|
||||
|
||||
// Parse the tool call XML to create proper assistant message content
|
||||
const parsedContent = parseAssistantMessage(toolCallXml)
|
||||
|
||||
// Add the parsed tool call to the assistant message content
|
||||
task.assistantMessageContent.push(...parsedContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/core/timeout/TimeoutManager.ts
Normal file
199
src/core/timeout/TimeoutManager.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { EventEmitter } from "events"
|
||||
import type { ToolName } from "@roo-code/types"
|
||||
|
||||
export interface TimeoutConfig {
|
||||
toolName: ToolName
|
||||
timeoutMs: number
|
||||
enableFallback: boolean
|
||||
taskId?: string
|
||||
}
|
||||
|
||||
export interface TimeoutResult<T> {
|
||||
success: boolean
|
||||
result?: T
|
||||
timedOut: boolean
|
||||
fallbackTriggered: boolean
|
||||
error?: Error
|
||||
executionTimeMs: number
|
||||
}
|
||||
|
||||
export interface TimeoutEvent {
|
||||
toolName: ToolName
|
||||
timeoutMs: number
|
||||
executionTimeMs: number
|
||||
taskId?: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages timeouts for all tool executions with configurable fallback mechanisms
|
||||
*/
|
||||
export class TimeoutManager extends EventEmitter {
|
||||
private static instance: TimeoutManager | undefined
|
||||
private activeOperations = new Map<string, AbortController>()
|
||||
/**
|
||||
* Multiple timeout events can be run at once
|
||||
* eg. running a command while reading a file
|
||||
*/
|
||||
private timeoutEvents: TimeoutEvent[] = []
|
||||
|
||||
private constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public static getInstance(): TimeoutManager {
|
||||
if (!TimeoutManager.instance) {
|
||||
TimeoutManager.instance = new TimeoutManager()
|
||||
}
|
||||
return TimeoutManager.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function with timeout protection
|
||||
*/
|
||||
public async executeWithTimeout<T>(
|
||||
operation: (signal: AbortSignal) => Promise<T>,
|
||||
config: TimeoutConfig,
|
||||
): Promise<TimeoutResult<T>> {
|
||||
const operationId = this.generateOperationId(config.toolName, config.taskId)
|
||||
const controller = new AbortController()
|
||||
const startTime = Date.now()
|
||||
|
||||
// Store the controller for potential cancellation
|
||||
this.activeOperations.set(operationId, controller)
|
||||
|
||||
try {
|
||||
// Create timeout promise
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort()
|
||||
reject(new Error(`Operation timed out after ${config.timeoutMs}ms`))
|
||||
}, config.timeoutMs)
|
||||
|
||||
// Clean up timeout if operation completes
|
||||
controller.signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeoutId)
|
||||
})
|
||||
})
|
||||
|
||||
// Race between operation and timeout
|
||||
const result = await Promise.race([operation(controller.signal), timeoutPromise])
|
||||
|
||||
const executionTimeMs = Date.now() - startTime
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
timedOut: false,
|
||||
fallbackTriggered: false,
|
||||
executionTimeMs,
|
||||
}
|
||||
} catch (error) {
|
||||
const executionTimeMs = Date.now() - startTime
|
||||
const timedOut = controller.signal.aborted
|
||||
|
||||
if (timedOut) {
|
||||
// Log timeout event
|
||||
const timeoutEvent: TimeoutEvent = {
|
||||
toolName: config.toolName,
|
||||
timeoutMs: config.timeoutMs,
|
||||
executionTimeMs,
|
||||
taskId: config.taskId,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
this.timeoutEvents.push(timeoutEvent)
|
||||
this.emit("timeout", timeoutEvent)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
timedOut: true,
|
||||
fallbackTriggered: config.enableFallback,
|
||||
error: error as Error,
|
||||
executionTimeMs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
timedOut: false,
|
||||
fallbackTriggered: false,
|
||||
error: error as Error,
|
||||
executionTimeMs,
|
||||
}
|
||||
} finally {
|
||||
// Clean up
|
||||
this.activeOperations.delete(operationId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a specific operation by tool name and task ID
|
||||
*/
|
||||
public cancelOperation(toolName: ToolName, taskId?: string): boolean {
|
||||
const operationId = this.generateOperationId(toolName, taskId)
|
||||
const controller = this.activeOperations.get(operationId)
|
||||
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.activeOperations.delete(operationId)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all active operations
|
||||
*/
|
||||
public cancelAllOperations(): void {
|
||||
for (const controller of this.activeOperations.values()) {
|
||||
controller.abort()
|
||||
}
|
||||
this.activeOperations.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timeout events for debugging/monitoring
|
||||
*/
|
||||
public getTimeoutEvents(limit = 100): TimeoutEvent[] {
|
||||
return this.timeoutEvents.slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear timeout event history
|
||||
*/
|
||||
public clearTimeoutEvents(): void {
|
||||
this.timeoutEvents = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active operation count
|
||||
*/
|
||||
public getActiveOperationCount(): number {
|
||||
return this.activeOperations.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific operation is active
|
||||
*/
|
||||
public isOperationActive(toolName: ToolName, taskId?: string): boolean {
|
||||
const operationId = this.generateOperationId(toolName, taskId)
|
||||
return this.activeOperations.has(operationId)
|
||||
}
|
||||
|
||||
private generateOperationId(toolName: ToolName, taskId?: string): string {
|
||||
return `${toolName}:${taskId || "default"}:${Date.now()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup method for graceful shutdown
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.cancelAllOperations()
|
||||
this.removeAllListeners()
|
||||
this.timeoutEvents = []
|
||||
}
|
||||
}
|
||||
|
||||
export const timeoutManager = TimeoutManager.getInstance()
|
||||
163
src/core/timeout/ToolExecutionWrapper.ts
Normal file
163
src/core/timeout/ToolExecutionWrapper.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import type { ToolName } from "@roo-code/types"
|
||||
import { timeoutManager, type TimeoutConfig, type TimeoutResult } from "./TimeoutManager"
|
||||
|
||||
export interface ToolExecutionOptions {
|
||||
toolName: ToolName
|
||||
taskId?: string
|
||||
timeoutMs?: number
|
||||
enableFallback?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for executing tools with timeout protection
|
||||
*/
|
||||
export class ToolExecutionWrapper {
|
||||
/**
|
||||
* Execute a tool operation with timeout protection
|
||||
*/
|
||||
public static async execute<T>(
|
||||
operation: (signal: AbortSignal) => Promise<T>,
|
||||
options: ToolExecutionOptions,
|
||||
defaultTimeoutMs = 300000, // 5 minutes default
|
||||
): Promise<TimeoutResult<T>> {
|
||||
const config: TimeoutConfig = {
|
||||
toolName: options.toolName,
|
||||
timeoutMs: options.timeoutMs ?? defaultTimeoutMs,
|
||||
enableFallback: options.enableFallback ?? true,
|
||||
taskId: options.taskId,
|
||||
}
|
||||
|
||||
return timeoutManager.executeWithTimeout(operation, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a promise-based operation to support AbortSignal
|
||||
*/
|
||||
public static wrapPromise<T>(promiseFactory: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// Check if already aborted
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Operation was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
// Execute the operation
|
||||
promiseFactory()
|
||||
.then((result) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
resolve(result)
|
||||
})
|
||||
.catch((error) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a callback-based operation to support AbortSignal
|
||||
*/
|
||||
public static wrapCallback<T>(
|
||||
operation: (callback: (error: Error | null, result?: T) => void, signal: AbortSignal) => void,
|
||||
signal: AbortSignal,
|
||||
): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// Check if already aborted
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
// Set up abort listener
|
||||
const abortListener = () => {
|
||||
reject(new Error("Operation was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
|
||||
// Execute the operation
|
||||
operation((error, result) => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(result!)
|
||||
}
|
||||
}, signal)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an abortable delay
|
||||
*/
|
||||
public static delay(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Delay was aborted before starting"))
|
||||
return
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
signal.removeEventListener("abort", abortListener)
|
||||
resolve()
|
||||
}, ms)
|
||||
|
||||
const abortListener = () => {
|
||||
clearTimeout(timeoutId)
|
||||
reject(new Error("Delay was aborted"))
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortListener)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute multiple operations in parallel with timeout protection
|
||||
*/
|
||||
public static async executeParallel<T>(
|
||||
operations: Array<{
|
||||
operation: (signal: AbortSignal) => Promise<T>
|
||||
options: ToolExecutionOptions
|
||||
}>,
|
||||
defaultTimeoutMs = 300000,
|
||||
): Promise<TimeoutResult<T>[]> {
|
||||
const promises = operations.map(({ operation, options }) =>
|
||||
ToolExecutionWrapper.execute(operation, options, defaultTimeoutMs),
|
||||
)
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute operations in sequence with timeout protection
|
||||
*/
|
||||
public static async executeSequential<T>(
|
||||
operations: Array<{
|
||||
operation: (signal: AbortSignal) => Promise<T>
|
||||
options: ToolExecutionOptions
|
||||
}>,
|
||||
defaultTimeoutMs = 300000,
|
||||
): Promise<TimeoutResult<T>[]> {
|
||||
const results: TimeoutResult<T>[] = []
|
||||
|
||||
for (const { operation, options } of operations) {
|
||||
const result = await ToolExecutionWrapper.execute(operation, options, defaultTimeoutMs)
|
||||
results.push(result)
|
||||
|
||||
// Stop execution if any operation fails or times out
|
||||
if (!result.success) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
234
src/core/timeout/__tests__/ai-fallback-real.spec.ts
Normal file
234
src/core/timeout/__tests__/ai-fallback-real.spec.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// npx vitest run src/core/timeout/__tests__/ai-fallback-real.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutFallbackGenerator } from "../TimeoutFallbackGenerator"
|
||||
import type { ApiHandler, SingleCompletionHandler } from "../../../api"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
// Create a mock API handler that extends ApiHandler and includes completePrompt
|
||||
interface MockApiHandler extends ApiHandler, SingleCompletionHandler {}
|
||||
|
||||
describe("TimeoutFallbackGenerator - Real AI Implementation", () => {
|
||||
let mockApiHandler: MockApiHandler
|
||||
let mockTask: Partial<Task>
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
|
||||
// Mock API handler that simulates real AI responses
|
||||
mockApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn(),
|
||||
}
|
||||
|
||||
// Mock task with API handler
|
||||
mockTask = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
})
|
||||
|
||||
test("should use AI to generate contextual suggestions when available", async () => {
|
||||
// Mock AI response with numbered suggestions
|
||||
const mockAiResponse = `Here are some suggestions for the timeout:
|
||||
|
||||
1. Break the npm install command into smaller package installations
|
||||
2. Clear npm cache and try again with npm cache clean --force
|
||||
3. Use npm install --no-optional to skip optional dependencies
|
||||
4. Check network connectivity and try with different registry`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("30 seconds")
|
||||
|
||||
// Check that AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Break the npm install command into smaller package installations")
|
||||
expect(followUp).toContain("Clear npm cache and try again")
|
||||
expect(followUp).toContain("Use npm install --no-optional")
|
||||
expect(followUp).toContain("Check network connectivity")
|
||||
|
||||
// Verify AI was called with proper prompt
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when AI fails", async () => {
|
||||
// Mock AI failure
|
||||
;(mockApiHandler.completePrompt as any).mockRejectedValueOnce(new Error("API Error"))
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 35000,
|
||||
toolParams: { command: "npm test" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
|
||||
// Should contain static fallback suggestions
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Break "npm test" into smaller')
|
||||
expect(followUp).toContain("background using")
|
||||
expect(followUp).toContain("alternative approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
})
|
||||
|
||||
test("should fallback to static suggestions when API handler is unavailable", async () => {
|
||||
// Task without API handler
|
||||
const taskWithoutApi = {}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 5000,
|
||||
executionTimeMs: 6000,
|
||||
toolParams: { path: "/large/file.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, taskWithoutApi as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/large/file.txt" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
})
|
||||
|
||||
test("should parse AI response with different numbering formats", async () => {
|
||||
// Test different numbering formats
|
||||
const mockAiResponse = `Here are the suggestions:
|
||||
|
||||
1) Try breaking the command into parts
|
||||
2. Use a different approach
|
||||
3) Check system resources
|
||||
4. Increase timeout duration`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: { command: "build script" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Try breaking the command into parts")
|
||||
expect(followUp).toContain("Use a different approach")
|
||||
expect(followUp).toContain("Check system resources")
|
||||
expect(followUp).toContain("Increase timeout duration")
|
||||
})
|
||||
|
||||
test("should handle AI response without numbered list", async () => {
|
||||
// AI response without clear numbering
|
||||
const mockAiResponse = `You could try splitting the operation. Another option is to check the network. Maybe increase the timeout. Consider using a different tool.`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16000,
|
||||
toolParams: { action: "click" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Should extract sentences as suggestions
|
||||
expect(followUp).toContain("You could try splitting the operation")
|
||||
expect(followUp).toContain("Another option is to check the network")
|
||||
})
|
||||
|
||||
test("should include task context in AI prompt when available", async () => {
|
||||
const mockAiResponse = `1. Try a different approach\n2. Check the working directory\n3. Break into steps`
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 20000,
|
||||
executionTimeMs: 22000,
|
||||
toolParams: { path: "/project", regex: ".*\\.ts$" },
|
||||
taskContext: {
|
||||
currentStep: "Finding TypeScript files",
|
||||
workingDirectory: "/project/src",
|
||||
previousActions: ["read package.json", "list files"],
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
|
||||
// Verify the prompt included task context
|
||||
const calledPrompt = (mockApiHandler.completePrompt as any).mock.calls[0][0]
|
||||
expect(calledPrompt).toContain("Current step: Finding TypeScript files")
|
||||
expect(calledPrompt).toContain("Working directory: /project/src")
|
||||
expect(calledPrompt).toContain("search_files")
|
||||
expect(calledPrompt).toContain("ts$") // Just check for the pattern ending
|
||||
})
|
||||
|
||||
test("should limit suggestions to maximum of 4", async () => {
|
||||
// AI response with many suggestions
|
||||
const mockAiResponse = `Here are many suggestions:
|
||||
|
||||
1. First suggestion
|
||||
2. Second suggestion
|
||||
3. Third suggestion
|
||||
4. Fourth suggestion
|
||||
5. Fifth suggestion
|
||||
6. Sixth suggestion
|
||||
7. Seventh suggestion`
|
||||
|
||||
;(mockApiHandler.completePrompt as any).mockResolvedValueOnce(mockAiResponse)
|
||||
|
||||
const context = {
|
||||
toolName: "write_to_file" as const,
|
||||
timeoutMs: 8000,
|
||||
executionTimeMs: 9000,
|
||||
toolParams: { path: "/output.txt" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
|
||||
// Count the number of <suggest> tags
|
||||
const suggestCount = (followUp.match(/<suggest>/g) || []).length
|
||||
expect(suggestCount).toBeLessThanOrEqual(4)
|
||||
|
||||
// Should include first 4 suggestions
|
||||
expect(followUp).toContain("First suggestion")
|
||||
expect(followUp).toContain("Fourth suggestion")
|
||||
// Should not include 5th and beyond
|
||||
expect(followUp).not.toContain("Fifth suggestion")
|
||||
})
|
||||
})
|
||||
179
src/core/timeout/__tests__/e2e-ai-test.spec.ts
Normal file
179
src/core/timeout/__tests__/e2e-ai-test.spec.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// End-to-end test to verify AI fallback generation works
|
||||
// npx vitest run src/core/timeout/__tests__/e2e-ai-test.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutFallbackGenerator } from "../TimeoutFallbackGenerator"
|
||||
import type { ApiHandler, SingleCompletionHandler } from "../../../api"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
// Mock API handler that simulates a real AI provider
|
||||
interface TestApiHandler extends ApiHandler, SingleCompletionHandler {}
|
||||
|
||||
describe("TimeoutFallbackGenerator - End-to-End AI Test", () => {
|
||||
test("should generate realistic AI suggestions for execute_command timeout", async () => {
|
||||
// Create a realistic mock API handler
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "claude-3-sonnet", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(150),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Here are some suggestions for the npm install timeout:
|
||||
|
||||
1. Clear npm cache with "npm cache clean --force" and retry
|
||||
2. Break installation into smaller chunks by installing packages individually
|
||||
3. Use "npm install --no-optional" to skip optional dependencies
|
||||
4. Check network connectivity and try with a different registry
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 60000,
|
||||
executionTimeMs: 65000,
|
||||
toolParams: {
|
||||
command: "npm install",
|
||||
cwd: "/project",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
// Verify the result structure
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
expect(result.toolCall?.params.question).toContain("60 seconds")
|
||||
|
||||
// Verify AI-generated suggestions are included
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Clear npm cache")
|
||||
expect(followUp).toContain("Break installation into smaller chunks")
|
||||
expect(followUp).toContain("no-optional")
|
||||
expect(followUp).toContain("network connectivity")
|
||||
|
||||
// Verify the AI was called with a proper prompt
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(
|
||||
expect.stringContaining("execute_command operation has timed out"),
|
||||
)
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("npm install"))
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalledWith(expect.stringContaining("60 seconds"))
|
||||
})
|
||||
|
||||
test("should handle AI response with different formatting", async () => {
|
||||
// Mock AI response with different numbering style
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "gpt-4", info: { maxTokens: 8192 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(200),
|
||||
completePrompt: vitest.fn().mockResolvedValue(
|
||||
`
|
||||
Based on the search_files timeout, here are my recommendations:
|
||||
|
||||
• Limit search to specific subdirectories instead of entire project
|
||||
• Use more specific regex patterns to reduce matches
|
||||
• Try list_files first to understand directory structure
|
||||
• Consider breaking search into multiple smaller operations
|
||||
`.trim(),
|
||||
),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "search_files" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 32000,
|
||||
toolParams: {
|
||||
path: "/large-project",
|
||||
regex: ".*",
|
||||
file_pattern: "*.ts",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should extract suggestions even with bullet points
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain("Narrow the search scope")
|
||||
expect(followUp).toContain("simpler search patterns")
|
||||
expect(followUp).toContain("file type filters")
|
||||
expect(followUp).toContain("incrementally in smaller batches")
|
||||
})
|
||||
|
||||
test("should gracefully handle AI failure and use static fallback", async () => {
|
||||
// Mock API handler that fails
|
||||
const mockApiHandler: TestApiHandler = {
|
||||
createMessage: vitest.fn(),
|
||||
getModel: vitest.fn().mockReturnValue({ id: "test-model", info: { maxTokens: 4096 } }),
|
||||
countTokens: vitest.fn().mockResolvedValue(100),
|
||||
completePrompt: vitest.fn().mockRejectedValue(new Error("API rate limit exceeded")),
|
||||
}
|
||||
|
||||
const mockTask: Partial<Task> = {
|
||||
api: mockApiHandler,
|
||||
}
|
||||
|
||||
const context = {
|
||||
toolName: "read_file" as const,
|
||||
timeoutMs: 10000,
|
||||
executionTimeMs: 12000,
|
||||
toolParams: {
|
||||
path: "/very/large/file.log",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for read_file
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Read "/very/large/file.log" in smaller chunks')
|
||||
expect(followUp).toContain("accessible and not locked")
|
||||
expect(followUp).toContain("different approach")
|
||||
expect(followUp).toContain("Increase the timeout")
|
||||
|
||||
// Verify AI was attempted but failed gracefully
|
||||
expect(mockApiHandler.completePrompt).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("should work without task API handler", async () => {
|
||||
// Task without API handler
|
||||
const mockTask: Partial<Task> = {}
|
||||
|
||||
const context = {
|
||||
toolName: "browser_action" as const,
|
||||
timeoutMs: 15000,
|
||||
executionTimeMs: 16500,
|
||||
toolParams: {
|
||||
action: "click",
|
||||
coordinate: "450,300",
|
||||
},
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context, mockTask as Task)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
|
||||
// Should contain static fallback suggestions for browser_action
|
||||
const followUp = result.toolCall?.params.follow_up || ""
|
||||
expect(followUp).toContain('Simplify the "click"')
|
||||
expect(followUp).toContain("Wait for specific elements")
|
||||
expect(followUp).toContain("direct API calls")
|
||||
expect(followUp).toContain("Reset the browser session")
|
||||
})
|
||||
})
|
||||
167
src/core/timeout/__tests__/timeout-integration.spec.ts
Normal file
167
src/core/timeout/__tests__/timeout-integration.spec.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// npx vitest run src/core/timeout/__tests__/timeout-integration.spec.ts
|
||||
|
||||
import { describe, test, expect, beforeEach, vitest } from "vitest"
|
||||
import { TimeoutManager } from "../TimeoutManager"
|
||||
import { ToolExecutionWrapper } from "../ToolExecutionWrapper"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import type { Task } from "../../task/Task"
|
||||
|
||||
describe("Timeout Integration Tests", () => {
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
})
|
||||
|
||||
test("TimeoutManager should handle basic timeout operations", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
// Test successful operation within timeout
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return "success"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toBe("success")
|
||||
expect(result.timedOut).toBe(false)
|
||||
})
|
||||
|
||||
test("TimeoutManager should handle timeout scenarios", async () => {
|
||||
const manager = TimeoutManager.getInstance()
|
||||
|
||||
// Test operation that times out
|
||||
const result = await manager.executeWithTimeout(
|
||||
async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return "should not reach here"
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.error?.message).toContain("Operation timed out")
|
||||
})
|
||||
|
||||
test("ToolExecutionWrapper should wrap operations correctly", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate checking abort signal
|
||||
if (signal.aborted) {
|
||||
throw new Error("Operation was aborted")
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
return [false, "test result"]
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 100,
|
||||
enableFallback: true,
|
||||
},
|
||||
100,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.result).toEqual([false, "test result"])
|
||||
expect(mockOperation).toHaveBeenCalledWith(expect.any(AbortSignal))
|
||||
})
|
||||
|
||||
test("ToolExecutionWrapper should handle timeout with fallback", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
return [false, "should not reach here"]
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50,
|
||||
enableFallback: true,
|
||||
},
|
||||
50,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
expect(result.fallbackTriggered).toBe(true)
|
||||
})
|
||||
|
||||
test("TimeoutFallbackHandler should create AI-powered responses", async () => {
|
||||
// Create a mock task to test tool injection
|
||||
const mockTask = {
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
} as unknown as Task
|
||||
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// The response should contain the basic timeout information
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("5 seconds")
|
||||
expect(response).toContain("6s")
|
||||
expect(response.length).toBeGreaterThan(50)
|
||||
|
||||
// The AI-generated tool call should be injected into the task's assistant message content
|
||||
const toolUseBlock = mockTask.assistantMessageContent.find((block) => block.type === "tool_use")
|
||||
expect(toolUseBlock).toBeDefined()
|
||||
if (toolUseBlock?.type === "tool_use") {
|
||||
expect(toolUseBlock.name).toBe("ask_followup_question")
|
||||
expect(toolUseBlock.params?.question).toContain("timed out")
|
||||
}
|
||||
})
|
||||
|
||||
test("AbortSignal should be properly handled", async () => {
|
||||
const mockOperation = vitest.fn().mockImplementation(async (signal: AbortSignal) => {
|
||||
// Simulate a long-running operation that checks abort signal
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
if (signal.aborted) {
|
||||
reject(new Error("Operation was aborted"))
|
||||
} else {
|
||||
resolve("success")
|
||||
}
|
||||
}, 100)
|
||||
|
||||
signal.addEventListener("abort", () => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error("Operation was aborted"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const result = await ToolExecutionWrapper.execute(
|
||||
mockOperation,
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: "test-task",
|
||||
timeoutMs: 50, // Shorter timeout to trigger abort
|
||||
enableFallback: false,
|
||||
},
|
||||
50,
|
||||
)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.timedOut).toBe(true)
|
||||
})
|
||||
})
|
||||
62
src/core/timeout/__tests__/tool-injection-test.spec.ts
Normal file
62
src/core/timeout/__tests__/tool-injection-test.spec.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, test, expect, vi, beforeEach } from "vitest"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
import { Task } from "../../task/Task"
|
||||
|
||||
describe("Tool Call Injection Test", () => {
|
||||
let mockTask: Task
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a minimal mock task with assistantMessageContent array
|
||||
mockTask = {
|
||||
assistantMessageContent: [],
|
||||
cwd: "/test/dir",
|
||||
} as unknown as Task
|
||||
})
|
||||
|
||||
test("should inject ask_followup_question tool call into assistant message content", async () => {
|
||||
// Mock the TimeoutFallbackGenerator to return a successful AI result
|
||||
const mockAiResult = {
|
||||
success: true,
|
||||
toolCall: {
|
||||
name: "ask_followup_question",
|
||||
params: {
|
||||
question: "What would you like to do next?",
|
||||
follow_up: "<suggest>Try a different approach</suggest><suggest>Break into smaller steps</suggest>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock the generateAiFallback method
|
||||
vi.doMock("../TimeoutFallbackGenerator", () => ({
|
||||
TimeoutFallbackGenerator: {
|
||||
generateAiFallback: vi.fn().mockResolvedValue(mockAiResult),
|
||||
},
|
||||
}))
|
||||
|
||||
// Call createTimeoutResponse
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
5000,
|
||||
6000,
|
||||
{ command: "npm install" },
|
||||
mockTask,
|
||||
)
|
||||
|
||||
// Check that the response is just the base timeout message
|
||||
expect(response).toContain("timed out after 5 seconds")
|
||||
expect(response).toContain("Execution Time: 6s")
|
||||
|
||||
// Check that the tool call was injected into assistantMessageContent
|
||||
// The XML parser might create multiple blocks (text + tool_use), so find the tool_use block
|
||||
const toolUseBlock = mockTask.assistantMessageContent.find((block) => block.type === "tool_use")
|
||||
expect(toolUseBlock).toBeDefined()
|
||||
expect(toolUseBlock?.type).toBe("tool_use")
|
||||
expect(toolUseBlock?.name).toBe("ask_followup_question")
|
||||
expect(toolUseBlock?.params?.question).toBeDefined()
|
||||
expect(toolUseBlock?.params?.follow_up).toBeDefined()
|
||||
|
||||
// Verify the question contains timeout information
|
||||
expect(toolUseBlock?.params?.question).toContain("timed out")
|
||||
expect(toolUseBlock?.params?.question).toContain("5 seconds")
|
||||
})
|
||||
})
|
||||
71
src/core/timeout/__tests__/ui-integration.spec.ts
Normal file
71
src/core/timeout/__tests__/ui-integration.spec.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { describe, it, expect, vi } from "vitest"
|
||||
import { TimeoutFallbackGenerator } from "../TimeoutFallbackGenerator"
|
||||
import { TimeoutFallbackHandler } from "../TimeoutFallbackHandler"
|
||||
|
||||
describe("UI Integration - AI Timeout Fallbacks", () => {
|
||||
it("should generate AI fallbacks using static method", async () => {
|
||||
const context = {
|
||||
toolName: "execute_command" as const,
|
||||
timeoutMs: 30000,
|
||||
executionTimeMs: 25000,
|
||||
toolParams: { command: "npm install" },
|
||||
}
|
||||
|
||||
const result = await TimeoutFallbackGenerator.generateAiFallback(context)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.toolCall).toBeDefined()
|
||||
expect(result.toolCall?.name).toBe("ask_followup_question")
|
||||
expect(result.toolCall?.params.question).toContain("execute_command")
|
||||
})
|
||||
|
||||
it("should create timeout response with AI fallbacks", async () => {
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm install",
|
||||
})
|
||||
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response.length).toBeGreaterThan(100) // Should contain substantial content
|
||||
})
|
||||
|
||||
it("should create timeout response when AI fallbacks fail", async () => {
|
||||
const response = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm install",
|
||||
})
|
||||
|
||||
expect(response).toContain("execute_command")
|
||||
expect(response).toContain("timed out")
|
||||
expect(response.length).toBeGreaterThan(50) // Should contain basic timeout message
|
||||
})
|
||||
|
||||
it("should validate UI setting flow", () => {
|
||||
// This test validates that timeout settings can be toggled
|
||||
const settings = {
|
||||
timeoutFallbackEnabled: true,
|
||||
toolExecutionTimeoutMs: 30000,
|
||||
}
|
||||
|
||||
// Simulate UI toggle
|
||||
settings.timeoutFallbackEnabled = false
|
||||
expect(settings.timeoutFallbackEnabled).toBe(false)
|
||||
|
||||
// Simulate timeout duration change
|
||||
settings.toolExecutionTimeoutMs = 60000
|
||||
expect(settings.toolExecutionTimeoutMs).toBe(60000)
|
||||
})
|
||||
|
||||
it("should handle different tool types with AI fallbacks", async () => {
|
||||
const commandResponse = await TimeoutFallbackHandler.createTimeoutResponse("execute_command", 30000, 25000, {
|
||||
command: "npm test",
|
||||
})
|
||||
|
||||
const browserResponse = await TimeoutFallbackHandler.createTimeoutResponse("browser_action", 30000, 25000, {
|
||||
action: "click",
|
||||
})
|
||||
|
||||
expect(commandResponse).toContain("execute_command")
|
||||
expect(browserResponse).toContain("browser_action")
|
||||
expect(commandResponse).not.toEqual(browserResponse)
|
||||
})
|
||||
})
|
||||
8
src/core/timeout/index.ts
Normal file
8
src/core/timeout/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { TimeoutManager, timeoutManager } from "./TimeoutManager"
|
||||
export { ToolExecutionWrapper } from "./ToolExecutionWrapper"
|
||||
export { TimeoutFallbackHandler } from "./TimeoutFallbackHandler"
|
||||
export { TimeoutFallbackGenerator } from "./TimeoutFallbackGenerator"
|
||||
|
||||
export type { TimeoutConfig, TimeoutResult, TimeoutEvent } from "./TimeoutManager"
|
||||
export type { ToolExecutionOptions } from "./ToolExecutionWrapper"
|
||||
export type { TimeoutFallbackContext, TimeoutFallbackResult } from "./TimeoutFallbackGenerator"
|
||||
|
|
@ -14,6 +14,7 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
|
|||
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
|
||||
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
||||
import { Terminal } from "../../integrations/terminal/Terminal"
|
||||
import { ToolExecutionWrapper, TimeoutFallbackHandler } from "../timeout"
|
||||
|
||||
class ShellIntegrationError extends Error {}
|
||||
|
||||
|
|
@ -60,7 +61,11 @@ export async function executeCommandTool(
|
|||
const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
|
||||
const clineProvider = await cline.providerRef.deref()
|
||||
const clineProviderState = await clineProvider?.getState()
|
||||
const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
|
||||
const {
|
||||
terminalOutputLineLimit = 500,
|
||||
terminalShellIntegrationDisabled = false,
|
||||
toolExecutionTimeoutMs = 300000, // 5 minutes default
|
||||
} = clineProviderState ?? {}
|
||||
|
||||
const options: ExecuteCommandOptions = {
|
||||
executionId,
|
||||
|
|
@ -68,6 +73,7 @@ export async function executeCommandTool(
|
|||
customCwd,
|
||||
terminalShellIntegrationDisabled,
|
||||
terminalOutputLineLimit,
|
||||
timeoutMs: toolExecutionTimeoutMs,
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -113,6 +119,7 @@ export type ExecuteCommandOptions = {
|
|||
customCwd?: string
|
||||
terminalShellIntegrationDisabled?: boolean
|
||||
terminalOutputLineLimit?: number
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
export async function executeCommand(
|
||||
|
|
@ -123,7 +130,66 @@ export async function executeCommand(
|
|||
customCwd,
|
||||
terminalShellIntegrationDisabled = false,
|
||||
terminalOutputLineLimit = 500,
|
||||
timeoutMs,
|
||||
}: ExecuteCommandOptions,
|
||||
): Promise<[boolean, ToolResponse]> {
|
||||
// Get timeout from settings if not provided
|
||||
const clineProvider = await cline.providerRef.deref()
|
||||
const clineProviderState = await clineProvider?.getState()
|
||||
const defaultTimeoutMs = clineProviderState?.toolExecutionTimeoutMs ?? 300000 // 5 minutes default
|
||||
const actualTimeoutMs = timeoutMs ?? defaultTimeoutMs
|
||||
const timeoutFallbackEnabled = clineProviderState?.timeoutFallbackEnabled ?? true
|
||||
|
||||
// Wrap the command execution with timeout
|
||||
const timeoutResult = await ToolExecutionWrapper.execute(
|
||||
async (signal: AbortSignal) => {
|
||||
return executeCommandInternal(cline, {
|
||||
executionId,
|
||||
command,
|
||||
customCwd,
|
||||
terminalShellIntegrationDisabled,
|
||||
terminalOutputLineLimit,
|
||||
signal,
|
||||
})
|
||||
},
|
||||
{
|
||||
toolName: "execute_command",
|
||||
taskId: cline.taskId,
|
||||
timeoutMs: actualTimeoutMs,
|
||||
enableFallback: timeoutFallbackEnabled,
|
||||
},
|
||||
actualTimeoutMs,
|
||||
)
|
||||
|
||||
// Handle timeout result
|
||||
if (timeoutResult.timedOut && timeoutResult.fallbackTriggered) {
|
||||
const fallbackResponse = await TimeoutFallbackHandler.createTimeoutResponse(
|
||||
"execute_command",
|
||||
actualTimeoutMs,
|
||||
timeoutResult.executionTimeMs,
|
||||
{ command },
|
||||
cline,
|
||||
)
|
||||
return [false, fallbackResponse]
|
||||
}
|
||||
|
||||
if (!timeoutResult.success) {
|
||||
return [false, formatResponse.toolError(timeoutResult.error?.message)]
|
||||
}
|
||||
|
||||
return timeoutResult.result!
|
||||
}
|
||||
|
||||
async function executeCommandInternal(
|
||||
cline: Task,
|
||||
{
|
||||
executionId,
|
||||
command,
|
||||
customCwd,
|
||||
terminalShellIntegrationDisabled = false,
|
||||
terminalOutputLineLimit = 500,
|
||||
signal,
|
||||
}: ExecuteCommandOptions & { signal: AbortSignal },
|
||||
): Promise<[boolean, ToolResponse]> {
|
||||
let workingDir: string
|
||||
|
||||
|
|
@ -211,8 +277,27 @@ export async function executeCommand(
|
|||
const process = terminal.runCommand(command, callbacks)
|
||||
cline.terminalProcess = process
|
||||
|
||||
await process
|
||||
cline.terminalProcess = undefined
|
||||
// Handle abort signal for timeout cancellation
|
||||
const abortHandler = () => {
|
||||
if (process && typeof process.abort === "function") {
|
||||
process.abort()
|
||||
}
|
||||
cline.terminalProcess = undefined
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
abortHandler()
|
||||
throw new Error("Command execution was cancelled due to timeout")
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", abortHandler)
|
||||
|
||||
try {
|
||||
await process
|
||||
} finally {
|
||||
signal.removeEventListener("abort", abortHandler)
|
||||
cline.terminalProcess = undefined
|
||||
}
|
||||
|
||||
if (shellIntegrationError) {
|
||||
throw new ShellIntegrationError(shellIntegrationError)
|
||||
|
|
|
|||
|
|
@ -1409,6 +1409,8 @@ export class ClineProvider
|
|||
profileThresholds,
|
||||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
toolExecutionTimeoutMs,
|
||||
timeoutFallbackEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
const telemetryKey = process.env.POSTHOG_API_KEY
|
||||
|
|
@ -1522,6 +1524,8 @@ export class ClineProvider
|
|||
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
|
||||
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
|
||||
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
|
||||
toolExecutionTimeoutMs: toolExecutionTimeoutMs ?? 300000,
|
||||
timeoutFallbackEnabled: timeoutFallbackEnabled ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1634,6 +1638,8 @@ export class ClineProvider
|
|||
terminalZshP10k: stateValues.terminalZshP10k ?? false,
|
||||
terminalZdotdir: stateValues.terminalZdotdir ?? false,
|
||||
terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
|
||||
toolExecutionTimeoutMs: stateValues.toolExecutionTimeoutMs ?? 300000, // 5 minutes default
|
||||
timeoutFallbackEnabled: stateValues.timeoutFallbackEnabled ?? false,
|
||||
mode: stateValues.mode ?? defaultModeSlug,
|
||||
language: stateValues.language ?? formatLanguage(vscode.env.language),
|
||||
mcpEnabled: stateValues.mcpEnabled ?? true,
|
||||
|
|
|
|||
|
|
@ -877,6 +877,14 @@ export const webviewMessageHandler = async (
|
|||
await updateGlobalState("terminalOutputLineLimit", message.value)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "toolExecutionTimeoutMs":
|
||||
await updateGlobalState("toolExecutionTimeoutMs", message.value)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "timeoutFallbackEnabled":
|
||||
await updateGlobalState("timeoutFallbackEnabled", message.bool)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
case "terminalShellIntegrationTimeout":
|
||||
await updateGlobalState("terminalShellIntegrationTimeout", message.value)
|
||||
await provider.postStateToWebview()
|
||||
|
|
|
|||
|
|
@ -227,6 +227,8 @@ export type ExtensionState = Pick<
|
|||
| "codebaseIndexConfig"
|
||||
| "codebaseIndexModels"
|
||||
| "profileThresholds"
|
||||
| "toolExecutionTimeoutMs"
|
||||
| "timeoutFallbackEnabled"
|
||||
> & {
|
||||
version: string
|
||||
clineMessages: ClineMessage[]
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ export interface WebviewMessage {
|
|||
| "terminalZshP10k"
|
||||
| "terminalZdotdir"
|
||||
| "terminalCompressProgressBar"
|
||||
| "toolExecutionTimeoutMs"
|
||||
| "timeoutFallbackEnabled"
|
||||
| "mcpEnabled"
|
||||
| "enableMcpServerCreation"
|
||||
| "searchCommits"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
alwaysAllowFollowupQuestions?: boolean
|
||||
followupAutoApproveTimeoutMs?: number
|
||||
allowedCommands?: string[]
|
||||
timeoutFallbackEnabled?: boolean
|
||||
toolExecutionTimeoutMs?: number
|
||||
setCachedStateField: SetCachedStateField<
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowReadOnlyOutsideWorkspace"
|
||||
|
|
@ -46,6 +48,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
| "followupAutoApproveTimeoutMs"
|
||||
| "allowedCommands"
|
||||
| "alwaysAllowUpdateTodoList"
|
||||
| "timeoutFallbackEnabled"
|
||||
| "toolExecutionTimeoutMs"
|
||||
>
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +70,8 @@ export const AutoApproveSettings = ({
|
|||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs = 60000,
|
||||
allowedCommands,
|
||||
timeoutFallbackEnabled,
|
||||
toolExecutionTimeoutMs,
|
||||
setCachedStateField,
|
||||
...props
|
||||
}: AutoApproveSettingsProps) => {
|
||||
|
|
@ -292,6 +298,50 @@ export const AutoApproveSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TIMEOUT SETTINGS */}
|
||||
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
|
||||
<div className="flex items-center gap-4 font-bold">
|
||||
<span className="codicon codicon-clock" />
|
||||
<div>{t("settings:autoApprove.timeout.label")}</div>
|
||||
</div>
|
||||
|
||||
{/* Enable timeout handling */}
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={timeoutFallbackEnabled}
|
||||
onChange={(e: any) => setCachedStateField("timeoutFallbackEnabled", e.target.checked)}
|
||||
data-testid="timeout-fallback-enabled-checkbox">
|
||||
<span className="font-medium">
|
||||
{t("settings:autoApprove.timeout.timeoutFallbackEnabled.label")}
|
||||
</span>
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
{t("settings:autoApprove.timeout.timeoutFallbackEnabled.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tool execution timeout duration */}
|
||||
<div>
|
||||
<div className="font-medium mb-2">
|
||||
{t("settings:autoApprove.timeout.toolExecutionTimeoutMs.label")}
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
min="1000"
|
||||
max="1800000"
|
||||
step="1000"
|
||||
value={toolExecutionTimeoutMs || 300000}
|
||||
onChange={(e) => setCachedStateField("toolExecutionTimeoutMs", parseInt(e.target.value))}
|
||||
disabled={!timeoutFallbackEnabled}
|
||||
className="w-32"
|
||||
data-testid="tool-execution-timeout-input"
|
||||
/>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
{t("settings:autoApprove.timeout.toolExecutionTimeoutMs.description")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -176,6 +176,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
profileThresholds,
|
||||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
timeoutFallbackEnabled,
|
||||
toolExecutionTimeoutMs,
|
||||
} = cachedState
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
|
@ -315,6 +317,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks })
|
||||
vscode.postMessage({ type: "alwaysAllowFollowupQuestions", bool: alwaysAllowFollowupQuestions })
|
||||
vscode.postMessage({ type: "followupAutoApproveTimeoutMs", value: followupAutoApproveTimeoutMs })
|
||||
vscode.postMessage({ type: "timeoutFallbackEnabled", bool: timeoutFallbackEnabled })
|
||||
vscode.postMessage({ type: "toolExecutionTimeoutMs", value: toolExecutionTimeoutMs })
|
||||
vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" })
|
||||
vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" })
|
||||
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
|
||||
|
|
@ -608,6 +612,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
|
||||
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
|
||||
allowedCommands={allowedCommands}
|
||||
timeoutFallbackEnabled={timeoutFallbackEnabled}
|
||||
toolExecutionTimeoutMs={toolExecutionTimeoutMs}
|
||||
setCachedStateField={setCachedStateField}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property
|
||||
followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions
|
||||
setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout
|
||||
timeoutFallbackEnabled?: boolean // New property for timeout fallback enabled
|
||||
setTimeoutFallbackEnabled: (value: boolean) => void // Setter for timeout fallback enabled
|
||||
toolExecutionTimeoutMs?: number // New property for tool execution timeout
|
||||
setToolExecutionTimeoutMs: (value: number) => void // Setter for tool execution timeout
|
||||
condensingApiConfigId?: string
|
||||
setCondensingApiConfigId: (value: string) => void
|
||||
customCondensingPrompt?: string
|
||||
|
|
@ -224,6 +228,9 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
},
|
||||
codebaseIndexModels: { ollama: {}, openai: {} },
|
||||
alwaysAllowUpdateTodoList: true,
|
||||
// Timeout settings
|
||||
timeoutFallbackEnabled: false, // Default to disabled
|
||||
toolExecutionTimeoutMs: 300000, // 5 minutes default
|
||||
})
|
||||
|
||||
const [didHydrateState, setDidHydrateState] = useState(false)
|
||||
|
|
@ -237,6 +244,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
const [marketplaceItems, setMarketplaceItems] = useState<any[]>([])
|
||||
const [alwaysAllowFollowupQuestions, setAlwaysAllowFollowupQuestions] = useState(false) // Add state for follow-up questions auto-approve
|
||||
const [followupAutoApproveTimeoutMs, setFollowupAutoApproveTimeoutMs] = useState<number | undefined>(undefined) // Will be set from global settings
|
||||
const [timeoutFallbackEnabled, setTimeoutFallbackEnabledState] = useState(false) // Add state for timeout fallback enabled
|
||||
const [toolExecutionTimeoutMs, setToolExecutionTimeoutMsState] = useState<number>(300000) // Add state for tool execution timeout (5 minutes default)
|
||||
const [marketplaceInstalledMetadata, setMarketplaceInstalledMetadata] = useState<MarketplaceInstalledMetadata>({
|
||||
project: {},
|
||||
global: {},
|
||||
|
|
@ -274,6 +283,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).followupAutoApproveTimeoutMs !== undefined) {
|
||||
setFollowupAutoApproveTimeoutMs((newState as any).followupAutoApproveTimeoutMs)
|
||||
}
|
||||
// Update timeout settings if present in state message
|
||||
if ((newState as any).timeoutFallbackEnabled !== undefined) {
|
||||
setTimeoutFallbackEnabledState((newState as any).timeoutFallbackEnabled)
|
||||
}
|
||||
if ((newState as any).toolExecutionTimeoutMs !== undefined) {
|
||||
setToolExecutionTimeoutMsState((newState as any).toolExecutionTimeoutMs)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
|
|
@ -373,6 +389,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
profileThresholds: state.profileThresholds ?? {},
|
||||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
timeoutFallbackEnabled,
|
||||
toolExecutionTimeoutMs,
|
||||
setExperimentEnabled: (id, enabled) =>
|
||||
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
|
||||
setApiConfiguration,
|
||||
|
|
@ -466,6 +484,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setAlwaysAllowUpdateTodoList: (value) => {
|
||||
setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value }))
|
||||
},
|
||||
setTimeoutFallbackEnabled: (value) => {
|
||||
setState((prevState) => ({ ...prevState, timeoutFallbackEnabled: value }))
|
||||
setTimeoutFallbackEnabledState(value)
|
||||
},
|
||||
setToolExecutionTimeoutMs: (value) => {
|
||||
setState((prevState) => ({ ...prevState, toolExecutionTimeoutMs: value }))
|
||||
setToolExecutionTimeoutMsState(value)
|
||||
},
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -170,6 +170,18 @@
|
|||
"title": "Max Requests",
|
||||
"description": "Automatically make this many API requests before asking for approval to continue with the task.",
|
||||
"unlimited": "Unlimited"
|
||||
},
|
||||
"timeout": {
|
||||
"label": "Timeout",
|
||||
"description": "Configure how Roo handles tool operations that exceed their timeout limits",
|
||||
"timeoutFallbackEnabled": {
|
||||
"label": "Enable timeout handling",
|
||||
"description": "Enable automatic timeout detection and fallback suggestions for long-running operations"
|
||||
},
|
||||
"toolExecutionTimeoutMs": {
|
||||
"label": "Tool execution timeout (ms)",
|
||||
"description": "Maximum time to wait for tool operations before triggering timeout handling (1000-1800000ms)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"providers": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue