diff --git a/rust/crates/runtime/src/bash.rs b/rust/crates/runtime/src/bash.rs index 331db2cf..dddf3ccf 100644 --- a/rust/crates/runtime/src/bash.rs +++ b/rust/crates/runtime/src/bash.rs @@ -177,33 +177,10 @@ async fn execute_bash_async( let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true); let output_result = if let Some(timeout_ms) = input.timeout { - match timeout(Duration::from_millis(timeout_ms), command.output()).await { - Ok(result) => (result?, false), - Err(_) => { - let is_test = is_test_command(&input.command); - let return_code_interpretation = if is_test { "test.hung" } else { "timeout" }; - return Ok(BashCommandOutput { - stdout: String::new(), - stderr: format!("Command exceeded timeout of {timeout_ms} ms"), - raw_output_path: None, - interrupted: true, - is_image: None, - background_task_id: None, - backgrounded_by_user: None, - assistant_auto_backgrounded: None, - dangerously_disable_sandbox: input.dangerously_disable_sandbox, - return_code_interpretation: Some(String::from(return_code_interpretation)), - no_output_expected: Some(true), - structured_content: Some(vec![test_timeout_provenance( - &input.command, - timeout_ms, - is_test, - )]), - persisted_output_path: None, - persisted_output_size: None, - sandbox_status: Some(sandbox_status), - }); - } + if let Ok(result) = timeout(Duration::from_millis(timeout_ms), command.output()).await { + (result?, false) + } else { + return Ok(timeout_output(&input, timeout_ms, sandbox_status)); } } else { (command.output().await?, false) @@ -240,6 +217,36 @@ async fn execute_bash_async( }) } +fn timeout_output( + input: &BashCommandInput, + timeout_ms: u64, + sandbox_status: SandboxStatus, +) -> BashCommandOutput { + let is_test = is_test_command(&input.command); + let return_code_interpretation = if is_test { "test.hung" } else { "timeout" }; + BashCommandOutput { + stdout: String::new(), + stderr: format!("Command exceeded timeout of {timeout_ms} ms"), + raw_output_path: None, + interrupted: true, + is_image: None, + background_task_id: None, + backgrounded_by_user: None, + assistant_auto_backgrounded: None, + dangerously_disable_sandbox: input.dangerously_disable_sandbox, + return_code_interpretation: Some(String::from(return_code_interpretation)), + no_output_expected: Some(true), + structured_content: Some(vec![test_timeout_provenance( + &input.command, + timeout_ms, + is_test, + )]), + persisted_output_path: None, + persisted_output_size: None, + sandbox_status: Some(sandbox_status), + } +} + fn is_test_command(command: &str) -> bool { let normalized = command .split_whitespace() diff --git a/rust/crates/runtime/src/recovery_recipes.rs b/rust/crates/runtime/src/recovery_recipes.rs index 82071a68..2ae64349 100644 --- a/rust/crates/runtime/src/recovery_recipes.rs +++ b/rust/crates/runtime/src/recovery_recipes.rs @@ -338,6 +338,7 @@ pub fn recipe_for(scenario: &FailureScenario) -> RecoveryRecipe { /// Looks up the recipe, enforces the one-attempt-before-escalation /// policy, simulates step execution (controlled by the context), and /// emits structured [`RecoveryEvent`]s for every attempt. +#[allow(clippy::too_many_lines)] pub fn attempt_recovery(scenario: &FailureScenario, ctx: &mut RecoveryContext) -> RecoveryResult { let recipe = recipe_for(scenario); let recipe_id = scenario.to_string();