Skip to content

Commit b7aea3d

Browse files
authored
warm shell PATH lookup during session init (#8631)
Signed-off-by: Bradley Axen <baxen@squareup.com>
1 parent 8a51e34 commit b7aea3d

1 file changed

Lines changed: 64 additions & 8 deletions

File tree

  • crates/goose/src/agents/platform_extensions/developer

crates/goose/src/agents/platform_extensions/developer/shell.rs

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,20 @@ use std::path::Path;
33
use std::path::PathBuf;
44
use std::process::Stdio;
55
use std::sync::atomic::{AtomicUsize, Ordering};
6-
use std::sync::LazyLock;
6+
#[cfg(not(windows))]
7+
use std::sync::Arc;
8+
#[cfg(not(windows))]
9+
use std::sync::Mutex;
710
use std::time::Duration;
811

912
use rmcp::model::{CallToolResult, Content};
1013
use schemars::JsonSchema;
1114
use serde::{Deserialize, Serialize};
1215
use tokio::io::{AsyncBufReadExt, BufReader};
16+
#[cfg(not(windows))]
17+
use tokio::sync::OnceCell;
18+
#[cfg(not(windows))]
19+
use tokio::task::JoinHandle;
1320
use tokio_stream::{wrappers::SplitStream, StreamExt};
1421

1522
use crate::subprocess::SubprocessExt;
@@ -197,14 +204,61 @@ fn resolve_login_shell_path() -> Option<String> {
197204
}
198205
}
199206

207+
/// Resolves the user's login-shell PATH in the background.
208+
///
209+
/// Spawned at `ShellTool` construction so the ~hundreds-of-ms cost of sourcing
210+
/// the user's shell profile overlaps with the rest of agent setup and the
211+
/// first LLM turn. The first `shell` invocation awaits the result; subsequent
212+
/// invocations read from the cached cell.
200213
#[cfg(not(windows))]
201-
static LOGIN_PATH: LazyLock<Option<String>> = LazyLock::new(resolve_login_shell_path);
214+
struct LoginPath {
215+
cell: OnceCell<Option<Arc<str>>>,
216+
handle: Mutex<Option<JoinHandle<Option<String>>>>,
217+
}
218+
219+
#[cfg(not(windows))]
220+
impl LoginPath {
221+
fn spawn() -> Self {
222+
let handle = tokio::task::spawn_blocking(resolve_login_shell_path);
223+
Self {
224+
cell: OnceCell::new(),
225+
handle: Mutex::new(Some(handle)),
226+
}
227+
}
228+
229+
#[cfg(test)]
230+
fn resolved(value: Option<String>) -> Self {
231+
let cell = OnceCell::new();
232+
let _ = cell.set(value.map(Arc::from));
233+
Self {
234+
cell,
235+
handle: Mutex::new(None),
236+
}
237+
}
238+
239+
async fn get(&self) -> Option<Arc<str>> {
240+
self.cell
241+
.get_or_init(|| async {
242+
let handle = self
243+
.handle
244+
.lock()
245+
.expect("login_path mutex poisoned")
246+
.take();
247+
match handle {
248+
Some(h) => h.await.ok().flatten().map(Arc::from),
249+
None => None,
250+
}
251+
})
252+
.await
253+
.clone()
254+
}
255+
}
202256

203257
pub struct ShellTool {
204258
output_dir: tempfile::TempDir,
205259
call_index: AtomicUsize,
206260
#[cfg(not(windows))]
207-
login_path: Option<String>,
261+
login_path: LoginPath,
208262
}
209263

210264
impl ShellTool {
@@ -213,7 +267,7 @@ impl ShellTool {
213267
output_dir: tempfile::tempdir()?,
214268
call_index: AtomicUsize::new(0),
215269
#[cfg(not(windows))]
216-
login_path: LOGIN_PATH.clone(),
270+
login_path: LoginPath::spawn(),
217271
})
218272
}
219273

@@ -223,7 +277,7 @@ impl ShellTool {
223277
output_dir: tempfile::tempdir()?,
224278
call_index: AtomicUsize::new(0),
225279
#[cfg(not(windows))]
226-
login_path: None,
280+
login_path: LoginPath::resolved(None),
227281
})
228282
}
229283

@@ -241,15 +295,17 @@ impl ShellTool {
241295
}
242296

243297
#[cfg(not(windows))]
244-
let login_path = self.login_path.as_deref();
298+
let login_path = self.login_path.get().await;
299+
#[cfg(not(windows))]
300+
let login_path_ref = login_path.as_deref();
245301
#[cfg(windows)]
246-
let login_path: Option<&str> = None;
302+
let login_path_ref: Option<&str> = None;
247303

248304
let execution = match run_command(
249305
&params.command,
250306
params.timeout_secs,
251307
working_dir,
252-
login_path,
308+
login_path_ref,
253309
)
254310
.await
255311
{

0 commit comments

Comments
 (0)