From f211ec86d9ac4c5c6f56ed0d19cb2974489ad74e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 27 Feb 2026 19:47:07 -0500 Subject: [PATCH] Cache ripgrep availability check in all execution environments The rg availability probe (rg --version / which rg) was running on every grep() call. Cache the result in a OnceLock/OnceCell per environment so the probe runs at most once. Also add grep -rn fallback to Daytona env, matching the pattern already used by local and Docker envs. Co-Authored-By: Claude Opus 4.6 --- crates/agent/src/docker_env.rs | 15 ++++----- crates/agent/src/local_env.rs | 19 ++++++----- crates/attractor/src/daytona_env.rs | 49 +++++++++++++++++++++-------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/crates/agent/src/docker_env.rs b/crates/agent/src/docker_env.rs index d2c0e1d7d..beecfd753 100644 --- a/crates/agent/src/docker_env.rs +++ b/crates/agent/src/docker_env.rs @@ -60,6 +60,7 @@ pub struct DockerExecutionEnvironment { container_id: tokio::sync::OnceCell, cached_platform: std::sync::OnceLock, cached_os_version: std::sync::OnceLock, + rg_available: tokio::sync::OnceCell, event_callback: Option, } @@ -77,6 +78,7 @@ impl DockerExecutionEnvironment { container_id: tokio::sync::OnceCell::new(), cached_platform: std::sync::OnceLock::new(), cached_os_version: std::sync::OnceLock::new(), + rg_available: tokio::sync::OnceCell::const_new(), event_callback: None, }) } @@ -581,16 +583,15 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { ) -> Result, String> { let container_path = self.resolve_container_path(path); - // Detect ripgrep availability - let (_, _, rg_check) = self - .docker_exec( + // Detect ripgrep availability (cached) + let use_rg = *self.rg_available.get_or_init(|| async { + let result = self.docker_exec( vec!["which".to_string(), "rg".to_string()], None, None, - ) - .await?; - - let use_rg = rg_check == 0; + ).await; + matches!(result, Ok((_, _, 0))) + }).await; let command = if use_rg { let mut args = vec!["rg".to_string(), "-n".to_string()]; diff --git a/crates/agent/src/local_env.rs b/crates/agent/src/local_env.rs index e38793597..df45fcd8a 100644 --- a/crates/agent/src/local_env.rs +++ b/crates/agent/src/local_env.rs @@ -9,12 +9,13 @@ use tokio_util::sync::CancellationToken; pub struct LocalExecutionEnvironment { working_directory: PathBuf, event_callback: Option, + rg_available: std::sync::OnceLock, } impl LocalExecutionEnvironment { #[must_use] pub fn new(working_directory: PathBuf) -> Self { - Self { working_directory, event_callback: None } + Self { working_directory, event_callback: None, rg_available: std::sync::OnceLock::new() } } pub fn set_event_callback(&mut self, cb: ExecEnvEventCallback) { @@ -230,13 +231,15 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { let full_path = self.resolve_path(path); // Try rg (ripgrep) first, fall back to grep - let use_rg = std::process::Command::new("rg") - .arg("--version") - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status() - .map(|s| s.success()) - .unwrap_or(false); + let use_rg = *self.rg_available.get_or_init(|| { + std::process::Command::new("rg") + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) + }); let output = if use_rg { let mut args = vec!["-n".to_string()]; diff --git a/crates/attractor/src/daytona_env.rs b/crates/attractor/src/daytona_env.rs index 323592030..5c1c54f7c 100644 --- a/crates/attractor/src/daytona_env.rs +++ b/crates/attractor/src/daytona_env.rs @@ -42,6 +42,7 @@ pub struct DaytonaExecutionEnvironment { config: DaytonaConfig, client: daytona_sdk::Client, sandbox: tokio::sync::OnceCell, + rg_available: tokio::sync::OnceCell, event_callback: Option, } @@ -52,6 +53,7 @@ impl DaytonaExecutionEnvironment { config, client, sandbox: tokio::sync::OnceCell::new(), + rg_available: tokio::sync::OnceCell::const_new(), event_callback: None, } } @@ -584,23 +586,44 @@ impl ExecutionEnvironment for DaytonaExecutionEnvironment { ) -> Result, String> { let resolved = self.resolve_path(path); - // Build rg command (same approach as Docker env) - let mut cmd = "rg --line-number --no-heading".to_string(); - if options.case_insensitive { - cmd.push_str(" -i"); - } - if let Some(ref glob_filter) = options.glob_filter { - cmd.push_str(&format!(" --glob '{glob_filter}'")); - } - if let Some(max) = options.max_results { - cmd.push_str(&format!(" --max-count {max}")); - } - cmd.push_str(&format!(" -- '{}' '{}'", pattern.replace('\'', "'\\''"), resolved)); + // Detect ripgrep availability (cached) + let use_rg = *self.rg_available.get_or_init(|| async { + let result = self.exec_command("rg --version", 10_000, None, None, None).await; + matches!(result, Ok(r) if r.exit_code == 0) + }).await; + + let cmd = if use_rg { + let mut cmd = "rg --line-number --no-heading".to_string(); + if options.case_insensitive { + cmd.push_str(" -i"); + } + if let Some(ref glob_filter) = options.glob_filter { + cmd.push_str(&format!(" --glob '{glob_filter}'")); + } + if let Some(max) = options.max_results { + cmd.push_str(&format!(" --max-count {max}")); + } + cmd.push_str(&format!(" -- '{}' '{}'", pattern.replace('\'', "'\\''"), resolved)); + cmd + } else { + let mut cmd = "grep -rn".to_string(); + if options.case_insensitive { + cmd.push_str(" -i"); + } + if let Some(ref glob_filter) = options.glob_filter { + cmd.push_str(&format!(" --include '{glob_filter}'")); + } + if let Some(max) = options.max_results { + cmd.push_str(&format!(" -m {max}")); + } + cmd.push_str(&format!(" -- '{}' '{}'", pattern.replace('\'', "'\\''"), resolved)); + cmd + }; let result = self.exec_command(&cmd, 30_000, None, None, None).await?; if result.exit_code == 1 { - // rg exits 1 for no matches + // Both rg and grep exit 1 for no matches return Ok(Vec::new()); } if result.exit_code != 0 {