diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 0ac0c4feb..7360827dd 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -1480,6 +1480,9 @@ async fn handle_serve_command(args: ServeCommandArgs) -> Result<()> { }) .collect::>>()?; let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key); + if let Err(error) = server.start_scheduler().await { + warn!("Scheduler failed to start; scheduled jobs will not run until a client connects: {error}"); + } let router = create_router( server, secret_key, diff --git a/crates/goose/src/acp/server_factory.rs b/crates/goose/src/acp/server_factory.rs index 752400b12..cb89febfd 100644 --- a/crates/goose/src/acp/server_factory.rs +++ b/crates/goose/src/acp/server_factory.rs @@ -32,6 +32,13 @@ impl AcpServer { } } + /// Start the scheduler now instead of on first client connect, so a + /// headless `goose serve` runs scheduled jobs; on failure `create_agent` + /// retries. No-op when the scheduler is disabled. + pub async fn start_scheduler(&self) -> Result<()> { + self.scheduler().await.map(|_| ()) + } + async fn scheduler(&self) -> Result>> { if !self.config.enable_scheduler { return Ok(None); @@ -57,6 +64,10 @@ impl AcpServer { let config = crate::config::Config::global(); let disable_session_naming = config.get_goose_disable_session_naming().unwrap_or(false); let scheduler = self.scheduler().await?; + if let Some(scheduler) = &scheduler { + // Listing syncs from storage, registering jobs persisted by other processes. + scheduler.list_scheduled_jobs().await; + } let provider_factory: AcpProviderFactory = Arc::new(move |provider_name, extensions, working_dir| { @@ -123,4 +134,32 @@ mod tests { assert!(server.scheduler().await.unwrap().is_some()); } + + #[tokio::test] + async fn start_scheduler_initializes_before_any_client_connects() { + let root = tempfile::tempdir().unwrap(); + let server = server(root.path().to_path_buf(), true); + + assert!(!server.scheduler.initialized()); + server.start_scheduler().await.unwrap(); + assert!(server.scheduler.initialized()); + } + + #[tokio::test] + async fn start_scheduler_is_idempotent() { + let root = tempfile::tempdir().unwrap(); + let server = server(root.path().to_path_buf(), true); + + server.start_scheduler().await.unwrap(); + server.start_scheduler().await.unwrap(); + } + + #[tokio::test] + async fn start_scheduler_does_not_construct_one_when_disabled() { + let root = tempfile::tempdir().unwrap(); + let server = server(root.path().to_path_buf(), false); + + server.start_scheduler().await.unwrap(); + assert!(!server.scheduler.initialized()); + } }