From 2e32857f072cdc91f25cf453a7dcb3cf34573822 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 22 Jul 2025 09:59:54 -0700 Subject: [PATCH 1/4] Use SIGKILL for command execution timeouts in the "execa" variant --- packages/evals/src/cli/runTask.ts | 23 ++++- src/core/tools/executeCommandTool.ts | 93 +++++++++---------- .../terminal/ExecaTerminalProcess.ts | 22 +++-- 3 files changed, 77 insertions(+), 61 deletions(-) diff --git a/packages/evals/src/cli/runTask.ts b/packages/evals/src/cli/runTask.ts index 0683cd72388..8142831c52c 100644 --- a/packages/evals/src/cli/runTask.ts +++ b/packages/evals/src/cli/runTask.ts @@ -5,7 +5,14 @@ import * as os from "node:os" import pWaitFor from "p-wait-for" import { execa } from "execa" -import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types" +import { + type TaskEvent, + type ClineSay, + TaskCommandName, + RooCodeEventName, + IpcMessageType, + EVALS_SETTINGS, +} from "@roo-code/types" import { IpcClient } from "@roo-code/ipc" import { @@ -203,6 +210,16 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) => log: [RooCodeEventName.TaskTokenUsageUpdated, RooCodeEventName.TaskAskResponded], } + const loggableSays: ClineSay[] = [ + "error", + "completion_result", + "command_output", + "rooignore_error", + "diff_error", + "condense_context", + "condense_context_error", + ] + client.on(IpcMessageType.TaskEvent, async (taskEvent) => { const { eventName, payload } = taskEvent @@ -215,7 +232,9 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) => // For message events we only log non-partial messages. if ( !ignoreEvents.log.includes(eventName) && - (eventName !== RooCodeEventName.Message || payload[0].message.partial !== true) + (eventName !== RooCodeEventName.Message || + (payload[0].message.say && loggableSays.includes(payload[0].message.say)) || + payload[0].message.partial !== true) ) { logger.info(`${eventName} ->`, payload) } diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index 81dc1993b2f..c346526a2e0 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -21,7 +21,7 @@ import { t } from "../../i18n" class ShellIntegrationError extends Error {} export async function executeCommandTool( - cline: Task, + task: Task, block: ToolUse, askApproval: AskApproval, handleError: HandleError, @@ -33,25 +33,25 @@ export async function executeCommandTool( try { if (block.partial) { - await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) + await task.ask("command", removeClosingTag("command", command), block.partial).catch(() => {}) return } else { if (!command) { - cline.consecutiveMistakeCount++ - cline.recordToolError("execute_command") - pushToolResult(await cline.sayAndCreateMissingParamError("execute_command", "command")) + task.consecutiveMistakeCount++ + task.recordToolError("execute_command") + pushToolResult(await task.sayAndCreateMissingParamError("execute_command", "command")) return } - const ignoredFileAttemptedToAccess = cline.rooIgnoreController?.validateCommand(command) + const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(command) if (ignoredFileAttemptedToAccess) { - await cline.say("rooignore_error", ignoredFileAttemptedToAccess) + await task.say("rooignore_error", ignoredFileAttemptedToAccess) pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess))) return } - cline.consecutiveMistakeCount = 0 + task.consecutiveMistakeCount = 0 command = unescapeHtmlEntities(command) // Unescape HTML entities. const didApprove = await askApproval("command", command) @@ -60,14 +60,15 @@ export async function executeCommandTool( return } - const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString() - const clineProvider = await cline.providerRef.deref() - const clineProviderState = await clineProvider?.getState() + const executionId = task.lastMessageTs?.toString() ?? Date.now().toString() + const provider = await task.providerRef.deref() + const providerState = await provider?.getState() + const { terminalOutputLineLimit = 500, terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT, terminalShellIntegrationDisabled = false, - } = clineProviderState ?? {} + } = providerState ?? {} // Get command execution timeout from VSCode configuration (in seconds) const commandExecutionTimeoutSeconds = vscode.workspace @@ -96,26 +97,26 @@ export async function executeCommandTool( } try { - const [rejected, result] = await executeCommand(cline, options) + const [rejected, result] = await executeCommand(task, options) if (rejected) { - cline.didRejectTool = true + task.didRejectTool = true } pushToolResult(result) } catch (error: unknown) { const status: CommandExecutionStatus = { executionId, status: "fallback" } - clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) - await cline.say("shell_integration_warning") + provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + await task.say("shell_integration_warning") if (error instanceof ShellIntegrationError) { - const [rejected, result] = await executeCommand(cline, { + const [rejected, result] = await executeCommand(task, { ...options, terminalShellIntegrationDisabled: true, }) if (rejected) { - cline.didRejectTool = true + task.didRejectTool = true } pushToolResult(result) @@ -143,7 +144,7 @@ export type ExecuteCommandOptions = { } export async function executeCommand( - cline: Task, + task: Task, { executionId, command, @@ -154,16 +155,16 @@ export async function executeCommand( commandExecutionTimeout = 0, }: ExecuteCommandOptions, ): Promise<[boolean, ToolResponse]> { - // Convert milliseconds back to seconds for display purposes + // Convert milliseconds back to seconds for display purposes. const commandExecutionTimeoutSeconds = commandExecutionTimeout / 1000 let workingDir: string if (!customCwd) { - workingDir = cline.cwd + workingDir = task.cwd } else if (path.isAbsolute(customCwd)) { workingDir = customCwd } else { - workingDir = path.resolve(cline.cwd, customCwd) + workingDir = path.resolve(task.cwd, customCwd) } try { @@ -180,7 +181,7 @@ export async function executeCommand( let shellIntegrationError: string | undefined const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode" - const clineProvider = await cline.providerRef.deref() + const provider = await task.providerRef.deref() let accumulatedOutput = "" const callbacks: RooTerminalCallbacks = { @@ -192,14 +193,14 @@ export async function executeCommand( terminalOutputCharacterLimit, ) const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput } - clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) if (runInBackground) { return } try { - const { response, text, images } = await cline.ask("command_output", "") + const { response, text, images } = await task.ask("command_output", "") runInBackground = true if (response === "messageResponse") { @@ -214,29 +215,30 @@ export async function executeCommand( terminalOutputLineLimit, terminalOutputCharacterLimit, ) - cline.say("command_output", result) + + task.say("command_output", result) completed = true }, onShellExecutionStarted: (pid: number | undefined) => { console.log(`[executeCommand] onShellExecutionStarted: ${pid}`) const status: CommandExecutionStatus = { executionId, status: "started", pid, command } - clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) }, onShellExecutionComplete: (details: ExitCodeDetails) => { const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode } - clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) exitDetails = details }, } if (terminalProvider === "vscode") { callbacks.onNoShellIntegration = async (error: string) => { - TelemetryService.instance.captureShellIntegrationError(cline.taskId) + TelemetryService.instance.captureShellIntegrationError(task.taskId) shellIntegrationError = error } } - const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider) + const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, task.taskId, terminalProvider) if (terminal instanceof Terminal) { terminal.terminal.show(true) @@ -248,9 +250,9 @@ export async function executeCommand( } const process = terminal.runCommand(command, callbacks) - cline.terminalProcess = process + task.terminalProcess = process - // Implement command execution timeout (skip if timeout is 0) + // Implement command execution timeout (skip if timeout is 0). if (commandExecutionTimeout > 0) { let timeoutId: NodeJS.Timeout | undefined let isTimedOut = false @@ -258,10 +260,7 @@ export async function executeCommand( const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { isTimedOut = true - // Try to abort the process - if (cline.terminalProcess) { - cline.terminalProcess.abort() - } + task.terminalProcess?.abort() reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`)) }, commandExecutionTimeout) }) @@ -270,17 +269,10 @@ export async function executeCommand( await Promise.race([process, timeoutPromise]) } catch (error) { if (isTimedOut) { - // Handle timeout case const status: CommandExecutionStatus = { executionId, status: "timeout" } - clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) - - // Add visual feedback for timeout - await cline.say( - "error", - t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds }), - ) - - cline.terminalProcess = undefined + provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) + await task.say("error", t("common:errors:command_timeout", { seconds: commandExecutionTimeoutSeconds })) + task.terminalProcess = undefined return [ false, @@ -292,14 +284,15 @@ export async function executeCommand( if (timeoutId) { clearTimeout(timeoutId) } - cline.terminalProcess = undefined + + task.terminalProcess = undefined } } else { - // No timeout - just wait for the process to complete + // No timeout - just wait for the process to complete. try { await process } finally { - cline.terminalProcess = undefined + task.terminalProcess = undefined } } @@ -316,7 +309,7 @@ export async function executeCommand( if (message) { const { text, images } = message - await cline.say("user_feedback", text, images) + await task.say("user_feedback", text, images) return [ true, diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 2b9b97d10aa..1c48d88aa67 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -73,6 +73,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { let timeoutId: NodeJS.Timeout | undefined const kill = new Promise((resolve) => { + console.log(`[ExecaTerminalProcess#run] SIGKILL -> ${this.pid}`) + timeoutId = setTimeout(() => { try { subprocess.kill("SIGKILL") @@ -86,7 +88,7 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { await Promise.race([subprocess, kill]) } catch (error) { console.log( - `[ExecaTerminalProcess] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`, + `[ExecaTerminalProcess#run] subprocess termination error: ${error instanceof Error ? error.message : String(error)}`, ) } @@ -98,12 +100,13 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { this.emit("shell_execution_complete", { exitCode: 0 }) } catch (error) { if (error instanceof ExecaError) { - console.error(`[ExecaTerminalProcess] shell execution error: ${error.message}`) + console.error(`[ExecaTerminalProcess#run] shell execution error: ${error.message}`) this.emit("shell_execution_complete", { exitCode: error.exitCode ?? 0, signalName: error.signal }) } else { console.error( - `[ExecaTerminalProcess] shell execution error: ${error instanceof Error ? error.message : String(error)}`, + `[ExecaTerminalProcess#run] shell execution error: ${error instanceof Error ? error.message : String(error)}`, ) + this.emit("shell_execution_complete", { exitCode: 1 }) } } @@ -128,29 +131,30 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { psTree(this.pid, async (err, children) => { if (!err) { const pids = children.map((p) => parseInt(p.PID)) + console.error(`[ExecaTerminalProcess#abort] SIGKILL children -> ${pids.join(", ")}`) for (const pid of pids) { try { - process.kill(pid, "SIGINT") + process.kill(pid, "SIGKILL") } catch (e) { console.warn( - `[ExecaTerminalProcess] Failed to send SIGINT to child PID ${pid}: ${e instanceof Error ? e.message : String(e)}`, + `[ExecaTerminalProcess#abort] Failed to send SIGKILL to child PID ${pid}: ${e instanceof Error ? e.message : String(e)}`, ) - // Optionally try SIGTERM or SIGKILL on failure, depending on desired behavior. } } } else { console.error( - `[ExecaTerminalProcess] Failed to get process tree for PID ${this.pid}: ${err.message}`, + `[ExecaTerminalProcess#abort] Failed to get process tree for PID ${this.pid}: ${err.message}`, ) } }) try { - process.kill(this.pid, "SIGINT") + console.error(`[ExecaTerminalProcess#abort] SIGKILL parent -> ${this.pid}`) + process.kill(this.pid, "SIGKILL") } catch (e) { console.warn( - `[ExecaTerminalProcess] Failed to send SIGINT to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`, + `[ExecaTerminalProcess#abort] Failed to send SIGKILL to main PID ${this.pid}: ${e instanceof Error ? e.message : String(e)}`, ) } } From 4abc283f616c49387e53439b66c9688d52268eeb Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 22 Jul 2025 11:01:01 -0700 Subject: [PATCH 2/4] More fixes --- apps/web-evals/src/actions/runs.ts | 2 +- packages/types/src/global-settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web-evals/src/actions/runs.ts b/apps/web-evals/src/actions/runs.ts index be4664d4d31..2eae1f6804a 100644 --- a/apps/web-evals/src/actions/runs.ts +++ b/apps/web-evals/src/actions/runs.ts @@ -56,7 +56,7 @@ export async function createRun({ suite, exercises = [], systemPrompt, timeout, const dockerArgs = [ `--name evals-controller-${run.id}`, - // "--rm", + "--rm", "--network evals_default", "-v /var/run/docker.sock:/var/run/docker.sock", "-v /tmp/evals:/var/log/evals", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 30521f2c685..94bbe7f48fb 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -222,7 +222,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { alwaysAllowUpdateTodoList: true, followupAutoApproveTimeoutMs: 0, allowedCommands: ["*"], - commandExecutionTimeout: 30_000, + commandExecutionTimeout: 30, commandTimeoutAllowlist: [], preventCompletionWithOpenTodos: false, From 436a6065495543b9b2ae3c78dbb8b9ec86c45de3 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 22 Jul 2025 12:28:18 -0700 Subject: [PATCH 3/4] Update evals defaults --- packages/evals/src/cli/runTask.ts | 1 - packages/types/src/global-settings.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/evals/src/cli/runTask.ts b/packages/evals/src/cli/runTask.ts index 8142831c52c..8b986e2afa0 100644 --- a/packages/evals/src/cli/runTask.ts +++ b/packages/evals/src/cli/runTask.ts @@ -212,7 +212,6 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) => const loggableSays: ClineSay[] = [ "error", - "completion_result", "command_output", "rooignore_error", "diff_error", diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 94bbe7f48fb..a49b00658b1 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -222,7 +222,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { alwaysAllowUpdateTodoList: true, followupAutoApproveTimeoutMs: 0, allowedCommands: ["*"], - commandExecutionTimeout: 30, + commandExecutionTimeout: 20, commandTimeoutAllowlist: [], preventCompletionWithOpenTodos: false, @@ -266,7 +266,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { mcpEnabled: false, - mode: "code", + mode: "architect", customModes: [], } From d465e6c7bf57a77e6dc77a201a8f2d0b965dbcaa Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 22 Jul 2025 23:50:25 -0700 Subject: [PATCH 4/4] Back to code mode in evals by default --- packages/types/src/global-settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a49b00658b1..906949919a5 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -266,7 +266,7 @@ export const EVALS_SETTINGS: RooCodeSettings = { mcpEnabled: false, - mode: "architect", + mode: "code", // "architect", customModes: [], }