Mnovich/temporal foreground tasks (#2895)

Co-authored-by: Carlos M. Lopez <carlopez@squareup.com>
This commit is contained in:
Max Novich
2025-06-20 16:19:58 -07:00
committed by GitHub
parent b3aed4bc11
commit 180b1df25d
58 changed files with 4009 additions and 1920 deletions
+1
View File
@@ -28,6 +28,7 @@ tokio = { version = "1.43", features = ["full"] }
futures = "0.3"
serde = { version = "1.0", features = ["derive"] } # For serialization
serde_yaml = "0.9"
tempfile = "3"
etcetera = "0.8.0"
reqwest = { version = "0.12.9", features = [
"rustls-tls-native-roots",
+14
View File
@@ -509,6 +509,16 @@ enum Command {
help = "Quiet mode. Suppress non-response output, printing only the model response to stdout"
)]
quiet: bool,
/// Scheduled job ID (used internally for scheduled executions)
#[arg(
long = "scheduled-job-id",
value_name = "ID",
help = "ID of the scheduled job that triggered this execution (internal use)",
long_help = "Internal parameter used when this run command is executed by a scheduled job. This associates the session with the schedule for tracking purposes.",
hide = true
)]
scheduled_job_id: Option<String>,
},
/// Recipe utilities for validation and deeplinking
@@ -662,6 +672,7 @@ pub async fn cli() -> Result<()> {
settings: None,
debug,
max_tool_repetitions,
scheduled_job_id: None,
interactive: true,
quiet: false,
})
@@ -709,6 +720,7 @@ pub async fn cli() -> Result<()> {
params,
explain,
render_recipe,
scheduled_job_id,
quiet,
}) => {
let (input_config, session_settings) = match (
@@ -808,6 +820,7 @@ pub async fn cli() -> Result<()> {
settings: session_settings,
debug,
max_tool_repetitions,
scheduled_job_id,
interactive, // Use the interactive flag from the Run command
quiet,
})
@@ -925,6 +938,7 @@ pub async fn cli() -> Result<()> {
settings: None::<SessionSettings>,
debug: false,
max_tool_repetitions: None,
scheduled_job_id: None,
interactive: true, // Default case is always interactive
quiet: false,
})
+1
View File
@@ -44,6 +44,7 @@ pub async fn agent_generator(
debug: false,
max_tool_repetitions: None,
interactive: false, // Benchmarking is non-interactive
scheduled_job_id: None,
quiet: false,
})
.await;
@@ -99,6 +99,7 @@ pub async fn handle_schedule_add(
paused: false,
current_session_id: None,
process_start_time: None,
execution_mode: Some("background".to_string()), // Default to background for CLI
};
let scheduler_storage_path =
+1
View File
@@ -464,6 +464,7 @@ async fn process_message_streaming(
id: session::Identifier::Path(session_file.clone()),
working_dir: std::env::current_dir()?,
schedule_id: None,
execution_mode: None,
};
// Get response from agent
+12 -2
View File
@@ -40,6 +40,8 @@ pub struct SessionBuilderConfig {
pub debug: bool,
/// Maximum number of consecutive identical tool calls allowed
pub max_tool_repetitions: Option<u32>,
/// ID of the scheduled job that triggered this session (if any)
pub scheduled_job_id: Option<String>,
/// Whether this session will be used interactively (affects debugging prompts)
pub interactive: bool,
/// Quiet mode - suppress non-response output
@@ -115,7 +117,7 @@ async fn offer_extension_debugging_help(
std::env::temp_dir().join(format!("goose_debug_extension_{}.jsonl", extension_name));
// Create the debugging session
let mut debug_session = Session::new(debug_agent, temp_session_file.clone(), false);
let mut debug_session = Session::new(debug_agent, temp_session_file.clone(), false, None);
// Process the debugging request
println!("{}", style("Analyzing the extension failure...").yellow());
@@ -341,7 +343,12 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> Session {
}
// Create new session
let mut session = Session::new(agent, session_file.clone(), session_config.debug);
let mut session = Session::new(
agent,
session_file.clone(),
session_config.debug,
session_config.scheduled_job_id.clone(),
);
// Add extensions if provided
for extension_str in session_config.extensions {
@@ -490,6 +497,7 @@ mod tests {
settings: None,
debug: true,
max_tool_repetitions: Some(5),
scheduled_job_id: None,
interactive: true,
quiet: false,
};
@@ -499,6 +507,7 @@ mod tests {
assert_eq!(config.builtins.len(), 1);
assert!(config.debug);
assert_eq!(config.max_tool_repetitions, Some(5));
assert!(config.scheduled_job_id.is_none());
assert!(config.interactive);
assert!(!config.quiet);
}
@@ -517,6 +526,7 @@ mod tests {
assert!(config.additional_system_prompt.is_none());
assert!(!config.debug);
assert!(config.max_tool_repetitions.is_none());
assert!(config.scheduled_job_id.is_none());
assert!(!config.interactive);
assert!(!config.quiet);
}
+46 -12
View File
@@ -51,6 +51,7 @@ pub struct Session {
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
debug: bool, // New field for debug mode
run_mode: RunMode,
scheduled_job_id: Option<String>, // ID of the scheduled job that triggered this session
}
// Cache structure for completion data
@@ -107,7 +108,12 @@ pub async fn classify_planner_response(
}
impl Session {
pub fn new(agent: Agent, session_file: PathBuf, debug: bool) -> Self {
pub fn new(
agent: Agent,
session_file: PathBuf,
debug: bool,
scheduled_job_id: Option<String>,
) -> Self {
let messages = match session::read_messages(&session_file) {
Ok(msgs) => msgs,
Err(e) => {
@@ -123,6 +129,7 @@ impl Session {
completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())),
debug,
run_mode: RunMode::Normal,
scheduled_job_id,
}
}
@@ -307,7 +314,13 @@ impl Session {
let provider = self.agent.provider().await?;
// Persist messages with provider for automatic description generation
session::persist_messages(&self.session_file, &self.messages, Some(provider)).await?;
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
Some(provider),
self.scheduled_job_id.clone(),
)
.await?;
// Track the current directory and last instruction in projects.json
let session_id = self
@@ -413,10 +426,11 @@ impl Session {
let provider = self.agent.provider().await?;
// Persist messages with provider for automatic description generation
session::persist_messages(
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
Some(provider),
self.scheduled_job_id.clone(),
)
.await?;
@@ -600,10 +614,11 @@ impl Session {
self.messages = summarized_messages;
// Persist the summarized messages
session::persist_messages(
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
Some(provider),
self.scheduled_job_id.clone(),
)
.await?;
@@ -727,7 +742,8 @@ impl Session {
id: session_id.clone(),
working_dir: std::env::current_dir()
.expect("failed to get current session working directory"),
schedule_id: None,
schedule_id: self.scheduled_job_id.clone(),
execution_mode: None,
}),
)
.await?;
@@ -776,7 +792,7 @@ impl Session {
Err(ToolError::ExecutionError("Tool call cancelled by user".to_string()))
));
self.messages.push(response_message);
session::persist_messages(&self.session_file, &self.messages, None).await?;
session::persist_messages_with_schedule_id(&self.session_file, &self.messages, None, self.scheduled_job_id.clone()).await?;
drop(stream);
break;
@@ -862,7 +878,8 @@ impl Session {
id: session_id.clone(),
working_dir: std::env::current_dir()
.expect("failed to get current session working directory"),
schedule_id: None,
schedule_id: self.scheduled_job_id.clone(),
execution_mode: None,
}),
)
.await?;
@@ -872,7 +889,7 @@ impl Session {
self.messages.push(message.clone());
// No need to update description on assistant messages
session::persist_messages(&self.session_file, &self.messages, None).await?;
session::persist_messages_with_schedule_id(&self.session_file, &self.messages, None, self.scheduled_job_id.clone()).await?;
if interactive {output::hide_thinking()};
let _ = progress_bars.hide();
@@ -1006,7 +1023,13 @@ impl Session {
self.messages.push(response_message);
// No need for description update here
session::persist_messages(&self.session_file, &self.messages, None).await?;
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
None,
self.scheduled_job_id.clone(),
)
.await?;
let prompt = format!(
"The existing call to {} was interrupted. How would you like to proceed?",
@@ -1015,7 +1038,13 @@ impl Session {
self.messages.push(Message::assistant().with_text(&prompt));
// No need for description update here
session::persist_messages(&self.session_file, &self.messages, None).await?;
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
None,
self.scheduled_job_id.clone(),
)
.await?;
output::render_message(&Message::assistant().with_text(&prompt), self.debug);
} else {
@@ -1029,8 +1058,13 @@ impl Session {
self.messages.push(Message::assistant().with_text(prompt));
// No need for description update here
session::persist_messages(&self.session_file, &self.messages, None)
.await?;
session::persist_messages_with_schedule_id(
&self.session_file,
&self.messages,
None,
self.scheduled_job_id.clone(),
)
.await?;
output::render_message(
&Message::assistant().with_text(prompt),
@@ -1,16 +0,0 @@
[package]
name = "goose-scheduler-executor"
version = "0.1.0"
edition = "2021"
[dependencies]
goose = { path = "../goose" }
mcp-core = { path = "../mcp-core" }
anyhow = "1.0"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
clap = { version = "4.0", features = ["derive"] }
futures = "0.3"
serde_json = "1.0"
serde_yaml = "0.9"
-215
View File
@@ -1,215 +0,0 @@
use anyhow::{anyhow, Result};
use clap::Parser;
use goose::agents::{Agent, SessionConfig};
use goose::config::Config;
use goose::message::Message;
use goose::providers::create;
use goose::recipe::Recipe;
use goose::session;
use std::env;
use std::fs;
use std::path::Path;
use tracing::info;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Job ID for the scheduled job
job_id: String,
/// Path to the recipe file to execute
recipe_path: String,
}
#[tokio::main]
async fn main() -> Result<()> {
// Initialize tracing
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
let args = Args::parse();
info!("Starting goose-scheduler-executor for job: {}", args.job_id);
info!("Recipe path: {}", args.recipe_path);
// Execute the recipe and get session ID
let session_id = execute_recipe(&args.job_id, &args.recipe_path).await?;
// Output session ID to stdout (this is what the Go service expects)
println!("{}", session_id);
Ok(())
}
async fn execute_recipe(job_id: &str, recipe_path: &str) -> Result<String> {
let recipe_path_buf = Path::new(recipe_path);
// Check if recipe file exists
if !recipe_path_buf.exists() {
return Err(anyhow!("Recipe file not found: {}", recipe_path));
}
// Read and parse recipe
let recipe_content = fs::read_to_string(recipe_path_buf)?;
let recipe: Recipe = {
let extension = recipe_path_buf
.extension()
.and_then(|os_str| os_str.to_str())
.unwrap_or("yaml")
.to_lowercase();
match extension.as_str() {
"json" | "jsonl" => serde_json::from_str::<Recipe>(&recipe_content)
.map_err(|e| anyhow!("Failed to parse JSON recipe '{}': {}", recipe_path, e))?,
"yaml" | "yml" => serde_yaml::from_str::<Recipe>(&recipe_content)
.map_err(|e| anyhow!("Failed to parse YAML recipe '{}': {}", recipe_path, e))?,
_ => {
return Err(anyhow!(
"Unsupported recipe file extension '{}' for: {}",
extension,
recipe_path
));
}
}
};
// Create agent
let agent = Agent::new();
// Get provider configuration
let global_config = Config::global();
let provider_name: String = global_config.get_param("GOOSE_PROVIDER").map_err(|_| {
anyhow!("GOOSE_PROVIDER not configured. Run 'goose configure' or set env var.")
})?;
let model_name: String = global_config.get_param("GOOSE_MODEL").map_err(|_| {
anyhow!("GOOSE_MODEL not configured. Run 'goose configure' or set env var.")
})?;
let model_config = goose::model::ModelConfig::new(model_name);
let provider = create(&provider_name, model_config)
.map_err(|e| anyhow!("Failed to create provider '{}': {}", provider_name, e))?;
// Set provider on agent
agent
.update_provider(provider)
.await
.map_err(|e| anyhow!("Failed to set provider on agent: {}", e))?;
info!(
"Agent configured with provider '{}' for job '{}'",
provider_name, job_id
);
// Generate session ID
let session_id = session::generate_session_id();
// Check if recipe has a prompt
let Some(prompt_text) = recipe.prompt else {
info!(
"Recipe '{}' has no prompt to execute for job '{}'",
recipe_path, job_id
);
// Create empty session for consistency
let session_file_path = goose::session::storage::get_path(
goose::session::storage::Identifier::Name(session_id.clone()),
);
let metadata = goose::session::storage::SessionMetadata {
working_dir: env::current_dir().unwrap_or_default(),
description: "Empty job - no prompt".to_string(),
schedule_id: Some(job_id.to_string()),
message_count: 0,
..Default::default()
};
goose::session::storage::save_messages_with_metadata(&session_file_path, &metadata, &[])
.map_err(|e| anyhow!("Failed to persist metadata for empty job: {}", e))?;
return Ok(session_id);
};
// Create session configuration
let current_dir =
env::current_dir().map_err(|e| anyhow!("Failed to get current directory: {}", e))?;
let session_config = SessionConfig {
id: goose::session::storage::Identifier::Name(session_id.clone()),
working_dir: current_dir.clone(),
schedule_id: Some(job_id.to_string()),
};
// Execute the recipe
let mut messages = vec![Message::user().with_text(prompt_text)];
info!("Executing recipe for job '{}' with prompt", job_id);
let mut stream = agent
.reply(&messages, Some(session_config))
.await
.map_err(|e| anyhow!("Agent failed to reply for recipe '{}': {}", recipe_path, e))?;
// Process the response stream
use futures::StreamExt;
use goose::agents::AgentEvent;
while let Some(message_result) = stream.next().await {
match message_result {
Ok(AgentEvent::Message(msg)) => {
if msg.role == mcp_core::role::Role::Assistant {
info!("[Job {}] Assistant response received", job_id);
}
messages.push(msg);
}
Ok(AgentEvent::McpNotification(_)) => {
// Handle notifications if needed
}
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
Err(e) => {
return Err(anyhow!("Error receiving message from agent: {}", e));
}
}
}
// Save session
let session_file_path = goose::session::storage::get_path(
goose::session::storage::Identifier::Name(session_id.clone()),
);
// Try to read updated metadata, or create fallback
match goose::session::storage::read_metadata(&session_file_path) {
Ok(mut updated_metadata) => {
updated_metadata.message_count = messages.len();
goose::session::storage::save_messages_with_metadata(
&session_file_path,
&updated_metadata,
&messages,
)
.map_err(|e| anyhow!("Failed to persist final messages: {}", e))?;
}
Err(_) => {
let fallback_metadata = goose::session::storage::SessionMetadata {
working_dir: current_dir,
description: format!("Scheduled job: {}", job_id),
schedule_id: Some(job_id.to_string()),
message_count: messages.len(),
..Default::default()
};
goose::session::storage::save_messages_with_metadata(
&session_file_path,
&fallback_metadata,
&messages,
)
.map_err(|e| anyhow!("Failed to persist messages with fallback metadata: {}", e))?;
}
}
info!(
"Finished executing job '{}', session: {}",
job_id, session_id
);
Ok(session_id)
}
+7 -2
View File
@@ -40,6 +40,7 @@ struct ChatRequest {
messages: Vec<Message>,
session_id: Option<String>,
session_working_dir: String,
scheduled_job_id: Option<String>,
}
pub struct SseResponse {
@@ -181,7 +182,8 @@ async fn handler(
Some(SessionConfig {
id: session::Identifier::Name(session_id.clone()),
working_dir: PathBuf::from(session_working_dir),
schedule_id: None,
schedule_id: request.scheduled_job_id.clone(),
execution_mode: None,
}),
)
.await
@@ -303,6 +305,7 @@ struct AskRequest {
prompt: String,
session_id: Option<String>,
session_working_dir: String,
scheduled_job_id: Option<String>,
}
#[derive(Debug, Serialize)]
@@ -339,7 +342,8 @@ async fn ask_handler(
Some(SessionConfig {
id: session::Identifier::Name(session_id.clone()),
working_dir: PathBuf::from(session_working_dir),
schedule_id: None,
schedule_id: request.scheduled_job_id.clone(),
execution_mode: None,
}),
)
.await
@@ -578,6 +582,7 @@ mod tests {
prompt: "test prompt".to_string(),
session_id: Some("test-session".to_string()),
session_working_dir: "test-working-dir".to_string(),
scheduled_job_id: None,
})
.unwrap(),
))
@@ -19,6 +19,8 @@ pub struct CreateScheduleRequest {
id: String,
recipe_source: String,
cron: String,
#[serde(default)]
execution_mode: Option<String>, // "foreground" or "background"
}
#[derive(Deserialize, Serialize, utoipa::ToSchema)]
@@ -124,6 +126,7 @@ async fn create_schedule(
paused: false,
current_session_id: None,
process_start_time: None,
execution_mode: req.execution_mode.or(Some("background".to_string())), // Default to background
};
scheduler
.add_scheduled_job(job.clone())
+19 -1
View File
@@ -574,7 +574,25 @@ impl Agent {
let (mut tools, mut toolshim_tools, mut system_prompt) =
self.prepare_tools_and_prompt().await?;
let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
// Get goose_mode from config, but override with execution_mode if provided in session config
let mut goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string());
// If this is a scheduled job with an execution_mode, override the goose_mode
if let Some(session_config) = &session {
if let Some(execution_mode) = &session_config.execution_mode {
// Map "foreground" to "auto" and "background" to "chat"
goose_mode = match execution_mode.as_str() {
"foreground" => "auto".to_string(),
"background" => "chat".to_string(),
_ => goose_mode,
};
tracing::info!(
"Using execution_mode '{}' which maps to goose_mode '{}'",
execution_mode,
goose_mode
);
}
}
let (tools_with_readonly_annotation, tools_without_annotation) =
Self::categorize_tools_by_annotation(&tools);
@@ -144,6 +144,7 @@ pub fn manage_schedule_tool() -> Tool {
"job_id": {"type": "string", "description": "Job identifier for operations on existing jobs"},
"recipe_path": {"type": "string", "description": "Path to recipe file for create action"},
"cron_expression": {"type": "string", "description": "A six field cron expression for create action"},
"execution_mode": {"type": "string", "description": "Execution mode for create action: 'foreground' or 'background'", "enum": ["foreground", "background"], "default": "background"},
"limit": {"type": "integer", "description": "Limit for sessions list", "default": 50},
"session_id": {"type": "string", "description": "Session identifier for session_content action"}
}
+17 -2
View File
@@ -94,6 +94,20 @@ impl Agent {
ToolError::ExecutionError("Missing 'cron_expression' parameter".to_string())
})?;
// Get the execution_mode parameter, defaulting to "background" if not provided
let execution_mode = arguments
.get("execution_mode")
.and_then(|v| v.as_str())
.unwrap_or("background");
// Validate execution_mode is either "foreground" or "background"
if execution_mode != "foreground" && execution_mode != "background" {
return Err(ToolError::ExecutionError(format!(
"Invalid execution_mode: {}. Must be 'foreground' or 'background'",
execution_mode
)));
}
// Validate recipe file exists and is readable
if !std::path::Path::new(recipe_path).exists() {
return Err(ToolError::ExecutionError(format!(
@@ -135,12 +149,13 @@ impl Agent {
paused: false,
current_session_id: None,
process_start_time: None,
execution_mode: Some(execution_mode.to_string()),
};
match scheduler.add_scheduled_job(job).await {
Ok(()) => Ok(vec![Content::text(format!(
"Successfully created scheduled job '{}' for recipe '{}' with cron expression '{}'",
job_id, recipe_path, cron_expression
"Successfully created scheduled job '{}' for recipe '{}' with cron expression '{}' in {} mode",
job_id, recipe_path, cron_expression, execution_mode
))]),
Err(e) => Err(ToolError::ExecutionError(format!(
"Failed to create job: {}",
+3 -1
View File
@@ -23,5 +23,7 @@ pub struct SessionConfig {
/// Working directory for the session
pub working_dir: PathBuf,
/// ID of the schedule that triggered this session, if any
pub schedule_id: Option<String>, // NEW
pub schedule_id: Option<String>,
/// Execution mode for scheduled jobs: "foreground" or "background"
pub execution_mode: Option<String>,
}
+8
View File
@@ -122,6 +122,8 @@ pub struct ScheduledJob {
pub current_session_id: Option<String>,
#[serde(default)]
pub process_start_time: Option<DateTime<Utc>>,
#[serde(default)]
pub execution_mode: Option<String>, // "foreground" or "background"
}
async fn persist_jobs_from_arc(
@@ -1059,6 +1061,10 @@ async fn run_scheduled_job_internal(
}
tracing::info!("Agent configured with provider for job '{}'", job.id);
// Log the execution mode
let execution_mode = job.execution_mode.as_deref().unwrap_or("background");
tracing::info!("Job '{}' running in {} mode", job.id, execution_mode);
let session_id_for_return = session::generate_session_id();
// Update the job with the session ID if we have access to the jobs arc
@@ -1091,6 +1097,7 @@ async fn run_scheduled_job_internal(
id: crate::session::storage::Identifier::Name(session_id_for_return.clone()),
working_dir: current_dir.clone(),
schedule_id: Some(job.id.clone()),
execution_mode: job.execution_mode.clone(),
};
match agent
@@ -1323,6 +1330,7 @@ mod tests {
paused: false,
current_session_id: None,
process_start_time: None,
execution_mode: Some("background".to_string()), // Default for test
};
// Create the mock provider instance for the test
+47 -79
View File
@@ -15,41 +15,33 @@ impl SchedulerType {
pub fn from_config() -> Self {
let config = Config::global();
// First check if alpha features are enabled
// If not, always use legacy scheduler regardless of GOOSE_SCHEDULER_TYPE
match config.get_param::<String>("ALPHA") {
Ok(alpha_value) => {
// Only proceed with temporal if alpha is explicitly enabled
if alpha_value.to_lowercase() != "true" {
tracing::info!("Alpha features disabled, using legacy scheduler");
return SchedulerType::Legacy;
}
}
Err(_) => {
// No ALPHA env var means alpha features are disabled
tracing::info!("No ALPHA environment variable found, using legacy scheduler");
return SchedulerType::Legacy;
}
}
// Debug logging to help troubleshoot environment variable issues
tracing::debug!("Checking scheduler configuration...");
// Alpha is enabled, now check scheduler type preference
// Check scheduler type preference from GOOSE_SCHEDULER_TYPE
match config.get_param::<String>("GOOSE_SCHEDULER_TYPE") {
Ok(scheduler_type) => match scheduler_type.to_lowercase().as_str() {
"temporal" => SchedulerType::Temporal,
"legacy" => SchedulerType::Legacy,
_ => {
tracing::warn!(
"Unknown scheduler type '{}', defaulting to legacy scheduler",
scheduler_type
);
SchedulerType::Legacy
Ok(scheduler_type) => {
tracing::debug!(
"Found GOOSE_SCHEDULER_TYPE environment variable: '{}'",
scheduler_type
);
match scheduler_type.to_lowercase().as_str() {
"temporal" => SchedulerType::Temporal,
"legacy" => SchedulerType::Legacy,
_ => {
tracing::warn!(
"Unknown scheduler type '{}', defaulting to legacy scheduler",
scheduler_type
);
SchedulerType::Legacy
}
}
},
}
Err(_) => {
// When alpha is enabled but no explicit scheduler type is set,
// default to temporal scheduler
tracing::info!("Alpha enabled, defaulting to temporal scheduler");
SchedulerType::Temporal
tracing::debug!("GOOSE_SCHEDULER_TYPE environment variable not found");
// When no explicit scheduler type is set, default to legacy scheduler
tracing::info!("No scheduler type specified, defaulting to legacy scheduler");
SchedulerType::Legacy
}
}
}
@@ -123,62 +115,38 @@ mod tests {
use temp_env::with_vars;
#[test]
fn test_scheduler_type_no_alpha_env() {
// Test that without ALPHA env var, we always get Legacy scheduler
with_vars(
[
("ALPHA", None::<&str>),
("GOOSE_SCHEDULER_TYPE", Some("temporal")),
],
|| {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
},
);
fn test_scheduler_type_no_env() {
// Test that without GOOSE_SCHEDULER_TYPE env var, we get Legacy scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", None::<&str>)], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
#[test]
fn test_scheduler_type_alpha_false() {
// Test that with ALPHA=false, we always get Legacy scheduler
with_vars(
[
("ALPHA", Some("false")),
("GOOSE_SCHEDULER_TYPE", Some("temporal")),
],
|| {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
},
);
fn test_scheduler_type_legacy() {
// Test that with GOOSE_SCHEDULER_TYPE=legacy, we get Legacy scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", Some("legacy"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
#[test]
fn test_scheduler_type_alpha_true_legacy() {
// Test that with ALPHA=true and GOOSE_SCHEDULER_TYPE=legacy, we get Legacy scheduler
with_vars(
[
("ALPHA", Some("true")),
("GOOSE_SCHEDULER_TYPE", Some("legacy")),
],
|| {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
},
);
fn test_scheduler_type_temporal() {
// Test that with GOOSE_SCHEDULER_TYPE=temporal, we get Temporal scheduler
with_vars([("GOOSE_SCHEDULER_TYPE", Some("temporal"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Temporal));
});
}
#[test]
fn test_scheduler_type_alpha_true_unknown_scheduler_type() {
// Test that with ALPHA=true and unknown scheduler type, we default to Legacy
with_vars(
[
("ALPHA", Some("true")),
("GOOSE_SCHEDULER_TYPE", Some("unknown")),
],
|| {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
},
);
fn test_scheduler_type_unknown() {
// Test that with unknown scheduler type, we default to Legacy
with_vars([("GOOSE_SCHEDULER_TYPE", Some("unknown"))], || {
let scheduler_type = SchedulerType::from_config();
assert!(matches!(scheduler_type, SchedulerType::Legacy));
});
}
}
+4 -3
View File
@@ -3,9 +3,10 @@ pub mod storage;
// Re-export common session types and functions
pub use storage::{
ensure_session_dir, generate_description, generate_session_id, get_most_recent_session,
get_path, list_sessions, persist_messages, read_messages, read_metadata, update_metadata,
Identifier, SessionMetadata,
ensure_session_dir, generate_description, generate_description_with_schedule_id,
generate_session_id, get_most_recent_session, get_path, list_sessions, persist_messages,
persist_messages_with_schedule_id, read_messages, read_metadata, update_metadata, Identifier,
SessionMetadata,
};
pub use info::{get_session_info, SessionInfo};
+37 -3
View File
@@ -437,6 +437,19 @@ pub async fn persist_messages(
session_file: &Path,
messages: &[Message],
provider: Option<Arc<dyn Provider>>,
) -> Result<()> {
persist_messages_with_schedule_id(session_file, messages, provider, None).await
}
/// Write messages to a session file with metadata, including an optional scheduled job ID
///
/// Overwrites the file with metadata as the first line, followed by all messages in JSONL format.
/// If a provider is supplied, it will automatically generate a description when appropriate.
pub async fn persist_messages_with_schedule_id(
session_file: &Path,
messages: &[Message],
provider: Option<Arc<dyn Provider>>,
schedule_id: Option<String>,
) -> Result<()> {
// Count user messages
let user_message_count = messages
@@ -448,11 +461,16 @@ pub async fn persist_messages(
match provider {
Some(provider) if user_message_count < 4 => {
//generate_description is responsible for writing the messages
generate_description(session_file, messages, provider).await
generate_description_with_schedule_id(session_file, messages, provider, schedule_id)
.await
}
_ => {
// Read existing metadata
let metadata = read_metadata(session_file)?;
let mut metadata = read_metadata(session_file)?;
// Update the schedule_id if provided
if schedule_id.is_some() {
metadata.schedule_id = schedule_id;
}
// Write the file with metadata and messages
save_messages_with_metadata(session_file, &metadata, messages)
}
@@ -492,6 +510,19 @@ pub async fn generate_description(
session_file: &Path,
messages: &[Message],
provider: Arc<dyn Provider>,
) -> Result<()> {
generate_description_with_schedule_id(session_file, messages, provider, None).await
}
/// Generate a description for the session using the provider, including an optional scheduled job ID
///
/// This function is called when appropriate to generate a short description
/// of the session based on the conversation history.
pub async fn generate_description_with_schedule_id(
session_file: &Path,
messages: &[Message],
provider: Arc<dyn Provider>,
schedule_id: Option<String>,
) -> Result<()> {
// Create a special message asking for a 3-word description
let mut description_prompt = "Based on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description".to_string();
@@ -527,8 +558,11 @@ pub async fn generate_description(
// Read current metadata
let mut metadata = read_metadata(session_file)?;
// Update description
// Update description and schedule_id
metadata.description = description;
if schedule_id.is_some() {
metadata.schedule_id = schedule_id;
}
// Update the file with the new metadata and existing messages
save_messages_with_metadata(session_file, &metadata, messages)
+335 -155
View File
@@ -16,8 +16,9 @@ use crate::session::storage::SessionMetadata;
const TEMPORAL_SERVICE_STARTUP_TIMEOUT: Duration = Duration::from_secs(15);
const TEMPORAL_SERVICE_HEALTH_CHECK_INTERVAL: Duration = Duration::from_millis(500);
// Default ports to try when discovering the service
const DEFAULT_HTTP_PORTS: &[u16] = &[8080, 8081, 8082, 8083, 8084, 8085];
// Default ports to try when discovering the service - using high, obscure ports
// to avoid conflicts with common services
const DEFAULT_HTTP_PORTS: &[u16] = &[58080, 58081, 58082, 58083, 58084, 58085];
#[derive(Serialize, Deserialize, Debug)]
struct JobRequest {
@@ -25,6 +26,7 @@ struct JobRequest {
job_id: Option<String>,
cron: Option<String>,
recipe_path: Option<String>,
execution_mode: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -45,6 +47,7 @@ struct TemporalJobStatus {
currently_running: bool,
paused: bool,
created_at: String,
execution_mode: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -52,13 +55,14 @@ struct RunNowResponse {
session_id: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PortConfig {
http_port: u16,
temporal_port: u16,
ui_port: u16,
}
#[derive(Clone)]
pub struct TemporalScheduler {
http_client: Client,
service_url: String,
@@ -107,50 +111,61 @@ impl TemporalScheduler {
port_config,
});
// Start the status monitor to keep job statuses in sync
if let Err(e) = final_scheduler.start_status_monitor().await {
tracing::warn!("Failed to start status monitor: {}", e);
}
info!("TemporalScheduler initialized successfully");
Ok(final_scheduler)
}
async fn discover_http_port(_http_client: &Client) -> Result<u16, SchedulerError> {
// First, try to find a running service using pgrep and lsof
if let Ok(port) = Self::find_temporal_service_port_from_processes() {
info!(
"Found Temporal service port {} from running processes",
port
);
return Ok(port);
}
// If no running service found, we need to find a free port to start the service on
info!("No running Temporal service found, finding free port to start service");
async fn discover_http_port(http_client: &Client) -> Result<u16, SchedulerError> {
info!("Discovering Temporal service port...");
// Check PORT environment variable first
if let Ok(port_str) = std::env::var("PORT") {
if let Ok(port) = port_str.parse::<u16>() {
if Self::is_port_free(port).await {
info!("Using PORT environment variable: {}", port);
if Self::is_temporal_service_running(http_client, port).await {
info!(
"Found running Temporal service on PORT environment variable: {}",
port
);
return Ok(port);
} else if Self::is_port_free(port).await {
info!("Using PORT environment variable for new service: {}", port);
return Ok(port);
} else {
warn!(
"PORT environment variable {} is not free, finding alternative",
"PORT environment variable {} is occupied by non-Temporal service",
port
);
}
}
}
// Try to find a free port from the default list
// Try to find an existing Temporal service on default ports
for &port in DEFAULT_HTTP_PORTS {
if Self::is_temporal_service_running(http_client, port).await {
info!("Found existing Temporal service on port {}", port);
return Ok(port);
}
}
// If no existing service found, find a free port to start a new one
info!("No existing Temporal service found, finding free port to start new service");
for &port in DEFAULT_HTTP_PORTS {
if Self::is_port_free(port).await {
info!("Found free port {} for Temporal service", port);
info!("Found free port {} for new Temporal service", port);
return Ok(port);
}
}
// If all default ports are taken, find any free port in a reasonable range
for port in 8086..8200 {
for port in 58086..58200 {
if Self::is_port_free(port).await {
info!("Found free port {} for Temporal service", port);
info!("Found free port {} for new Temporal service", port);
return Ok(port);
}
}
@@ -160,112 +175,51 @@ impl TemporalScheduler {
))
}
async fn is_port_free(port: u16) -> bool {
use std::net::{SocketAddr, TcpListener};
use std::time::Duration;
/// Check if a Temporal service is running and responding on the given port
async fn is_temporal_service_running(http_client: &Client, port: u16) -> bool {
let health_url = format!("http://127.0.0.1:{}/health", port);
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
// First, try to bind to the port
let listener_result = TcpListener::bind(addr);
match listener_result {
Ok(listener) => {
// Successfully bound, so port was free
drop(listener); // Release the port immediately
// Double-check by trying to connect to see if anything is actually listening
let client = reqwest::Client::builder()
.timeout(Duration::from_millis(500))
.build()
.unwrap();
let test_url = format!("http://127.0.0.1:{}", port);
match client.get(&test_url).send().await {
Ok(_) => {
// Something responded, so port is actually in use
warn!(
"Port {} appeared free but something is listening on it",
port
);
false
}
Err(_) => {
// Nothing responded, port is truly free
true
}
}
match http_client
.get(&health_url)
.timeout(Duration::from_millis(1000))
.send()
.await
{
Ok(response) if response.status().is_success() => {
info!("Confirmed Temporal service is running on port {}", port);
true
}
Ok(response) => {
info!(
"Port {} is responding but not a healthy Temporal service (status: {})",
port,
response.status()
);
false
}
Err(_) => {
// Could not bind, port is definitely in use
// Port might be free or occupied by something else
false
}
}
}
fn find_temporal_service_port_from_processes() -> Result<u16, SchedulerError> {
// Use pgrep to find temporal-service processes
let pgrep_output = Command::new("pgrep")
.arg("-f")
.arg("temporal-service")
.output()
.map_err(|e| SchedulerError::SchedulerInternalError(format!("pgrep failed: {}", e)))?;
async fn is_port_free(port: u16) -> bool {
use std::net::{SocketAddr, TcpListener};
if !pgrep_output.status.success() {
return Err(SchedulerError::SchedulerInternalError(
"No temporal-service processes found".to_string(),
));
}
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
let pids_str = String::from_utf8_lossy(&pgrep_output.stdout);
let pids: Vec<&str> = pids_str
.trim()
.split('\n')
.filter(|s| !s.is_empty())
.collect();
for pid in pids {
// Use lsof to find listening ports for this PID
let lsof_output = Command::new("lsof")
.arg("-p")
.arg(pid)
.arg("-i")
.arg("tcp")
.arg("-P") // Show port numbers instead of service names
.arg("-n") // Show IP addresses instead of hostnames
.output();
if let Ok(output) = lsof_output {
let lsof_str = String::from_utf8_lossy(&output.stdout);
// Look for HTTP API port (typically 8080-8999 range)
for line in lsof_str.lines() {
if line.contains("LISTEN") && line.contains("temporal-") {
// Parse lines like: "temporal-service 12345 user 6u IPv4 0x... 0t0 TCP *:8081 (LISTEN)"
let parts: Vec<&str> = line.split_whitespace().collect();
// Find the TCP part which contains the port
for part in &parts {
if part.starts_with("TCP") && part.contains(':') {
// Extract port from TCP *:8081 or TCP 127.0.0.1:8081
if let Some(port_str) = part.split(':').next_back() {
if let Ok(port) = port_str.parse::<u16>() {
// HTTP API ports are typically in 8080-8999 range
if (8080..9000).contains(&port) {
info!("Found HTTP API port {} for PID {}", port, pid);
return Ok(port);
}
}
}
}
}
}
}
// Try to bind to the port
match TcpListener::bind(addr) {
Ok(_listener) => {
// Successfully bound, so port is free
true
}
Err(_) => {
// Could not bind, port is in use
false
}
}
Err(SchedulerError::SchedulerInternalError(
"Could not find HTTP API port from temporal-service processes".to_string(),
))
}
async fn fetch_port_config(&self) -> Result<PortConfig, SchedulerError> {
@@ -305,7 +259,7 @@ impl TemporalScheduler {
self.port_config.temporal_port
}
/// Get the HTTP API port
/// Get the HTTP API port
pub fn get_http_port(&self) -> u16 {
self.port_config.http_port
}
@@ -366,7 +320,7 @@ impl TemporalScheduler {
command.process_group(0);
}
let child = command.spawn().map_err(|e| {
let mut child = command.spawn().map_err(|e| {
SchedulerError::SchedulerInternalError(format!(
"Failed to start Go temporal service: {}",
e
@@ -379,9 +333,6 @@ impl TemporalScheduler {
pid, self.port_config.http_port
);
// Don't wait for the child process - let it run independently
std::mem::forget(child);
// Give the process a moment to start up
sleep(Duration::from_millis(100)).await;
@@ -410,6 +361,12 @@ impl TemporalScheduler {
}
}
// Detach the child process by not waiting for it
// This allows it to continue running independently
std::thread::spawn(move || {
let _ = child.wait();
});
Ok(())
}
@@ -535,6 +492,7 @@ impl TemporalScheduler {
job_id: Some(job.id.clone()),
cron: Some(job.cron.clone()),
recipe_path: Some(job.source.clone()),
execution_mode: job.execution_mode.clone(),
};
let response = self.make_request(request).await?;
@@ -554,6 +512,7 @@ impl TemporalScheduler {
job_id: None,
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
@@ -572,6 +531,7 @@ impl TemporalScheduler {
paused: tj.paused,
current_session_id: None, // Not provided by Temporal service
process_start_time: None, // Not provided by Temporal service
execution_mode: tj.execution_mode,
}
})
.collect();
@@ -587,6 +547,7 @@ impl TemporalScheduler {
job_id: Some(id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
@@ -605,6 +566,7 @@ impl TemporalScheduler {
job_id: Some(id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
@@ -623,6 +585,7 @@ impl TemporalScheduler {
job_id: Some(id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
@@ -642,6 +605,7 @@ impl TemporalScheduler {
job_id: Some(id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
@@ -717,20 +681,172 @@ impl TemporalScheduler {
pub async fn update_schedule(
&self,
_sched_id: &str,
_new_cron: String,
sched_id: &str,
new_cron: String,
) -> Result<(), SchedulerError> {
warn!("update_schedule() method not implemented for TemporalScheduler - delete and recreate job instead");
Err(SchedulerError::SchedulerInternalError(
"update_schedule not supported - delete and recreate job instead".to_string(),
))
tracing::info!(
"TemporalScheduler: update_schedule() called for job '{}' with cron '{}'",
sched_id,
new_cron
);
let request = JobRequest {
action: "update".to_string(),
job_id: Some(sched_id.to_string()),
cron: Some(new_cron),
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
if response.success {
info!("Successfully updated scheduled job: {}", sched_id);
Ok(())
} else {
Err(SchedulerError::SchedulerInternalError(response.message))
}
}
pub async fn kill_running_job(&self, _sched_id: &str) -> Result<(), SchedulerError> {
warn!("kill_running_job() method not implemented for TemporalScheduler");
Err(SchedulerError::SchedulerInternalError(
"kill_running_job not supported by TemporalScheduler".to_string(),
))
pub async fn kill_running_job(&self, sched_id: &str) -> Result<(), SchedulerError> {
tracing::info!(
"TemporalScheduler: kill_running_job() called for job '{}'",
sched_id
);
let request = JobRequest {
action: "kill_job".to_string(),
job_id: Some(sched_id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
let response = self.make_request(request).await?;
if response.success {
info!("Successfully killed running job: {}", sched_id);
Ok(())
} else {
Err(SchedulerError::SchedulerInternalError(response.message))
}
}
pub async fn update_job_status_from_sessions(&self) -> Result<(), SchedulerError> {
tracing::info!("TemporalScheduler: Checking job status based on session activity");
let jobs = self.list_scheduled_jobs().await?;
for job in jobs {
if job.currently_running {
// First, check with the Temporal service directly for the most accurate status
let request = JobRequest {
action: "status".to_string(),
job_id: Some(job.id.clone()),
cron: None,
recipe_path: None,
execution_mode: None,
};
match self.make_request(request).await {
Ok(response) => {
if response.success {
if let Some(jobs) = response.jobs {
if let Some(temporal_job) = jobs.iter().find(|j| j.id == job.id) {
// If Temporal service says it's not running, trust that
if !temporal_job.currently_running {
tracing::info!(
"Temporal service reports job '{}' is not running",
job.id
);
continue; // Job is already marked as not running by Temporal
}
}
}
}
}
Err(e) => {
tracing::warn!(
"Failed to get status from Temporal service for job '{}': {}",
job.id,
e
);
// Fall back to session-based checking if Temporal service is unavailable
}
}
// Secondary check: look for recent session activity (more lenient timing)
let recent_sessions = self.sessions(&job.id, 3).await?;
let mut has_active_session = false;
for (session_name, _) in recent_sessions {
let session_path = crate::session::storage::get_path(
crate::session::storage::Identifier::Name(session_name),
);
// Check if session file was modified recently (within last 5 minutes instead of 2)
if let Ok(metadata) = std::fs::metadata(&session_path) {
if let Ok(modified) = metadata.modified() {
let modified_dt: DateTime<Utc> = modified.into();
let now = Utc::now();
let time_diff = now.signed_duration_since(modified_dt);
// Increased tolerance to 5 minutes to reduce false positives
if time_diff.num_minutes() < 5 {
has_active_session = true;
tracing::debug!(
"Found active session for job '{}' modified {} minutes ago",
job.id,
time_diff.num_minutes()
);
break;
}
}
}
}
// Only mark as completed if both Temporal service check failed AND no recent session activity
if !has_active_session {
tracing::info!(
"No active sessions found for job '{}' in the last 5 minutes, marking as completed",
job.id
);
let request = JobRequest {
action: "mark_completed".to_string(),
job_id: Some(job.id.clone()),
cron: None,
recipe_path: None,
execution_mode: None,
};
if let Err(e) = self.make_request(request).await {
tracing::warn!("Failed to mark job '{}' as completed: {}", job.id, e);
}
}
}
}
Ok(())
}
/// Periodically check and update job statuses based on session activity
pub async fn start_status_monitor(&self) -> Result<(), SchedulerError> {
let scheduler_clone = self.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Check every 60 seconds instead of 30
loop {
interval.tick().await;
if let Err(e) = scheduler_clone.update_job_status_from_sessions().await {
tracing::warn!("Failed to update job statuses: {}", e);
}
}
});
Ok(())
}
pub async fn get_running_job_info(
@@ -742,24 +858,62 @@ impl TemporalScheduler {
sched_id
);
// First check if the job is marked as currently running
let jobs = self.list_scheduled_jobs().await?;
let job = jobs.iter().find(|j| j.id == sched_id);
// Get the current job status from Temporal service
let request = JobRequest {
action: "status".to_string(),
job_id: Some(sched_id.to_string()),
cron: None,
recipe_path: None,
execution_mode: None,
};
if let Some(job) = job {
if job.currently_running {
// For now, we'll return a placeholder session ID and current time
// In a more complete implementation, we would track the actual session ID
// and start time from the Temporal workflow execution
let session_id =
format!("temporal-{}-{}", sched_id, chrono::Utc::now().timestamp());
let start_time = chrono::Utc::now(); // This should be the actual start time
Ok(Some((session_id, start_time)))
let response = self.make_request(request).await?;
if response.success {
if let Some(jobs) = response.jobs {
if let Some(job) = jobs.iter().find(|j| j.id == sched_id) {
if job.currently_running {
// Try to get the actual session ID from recent sessions
let recent_sessions = self.sessions(sched_id, 1).await?;
if let Some((session_name, _session_metadata)) = recent_sessions.first() {
// Check if this session is still active by looking at the session file
let session_path = crate::session::storage::get_path(
crate::session::storage::Identifier::Name(session_name.clone()),
);
// If the session file was modified recently (within last 5 minutes),
// consider it as the current running session
if let Ok(metadata) = std::fs::metadata(&session_path) {
if let Ok(modified) = metadata.modified() {
let modified_dt: DateTime<Utc> = modified.into();
let now = Utc::now();
let time_diff = now.signed_duration_since(modified_dt);
if time_diff.num_minutes() < 5 {
// This looks like an active session
return Ok(Some((session_name.clone(), modified_dt)));
}
}
}
}
// Fallback: return a temporal session ID with current time
let session_id =
format!("temporal-{}-{}", sched_id, Utc::now().timestamp());
let start_time = Utc::now();
Ok(Some((session_id, start_time)))
} else {
Ok(None)
}
} else {
Err(SchedulerError::JobNotFound(sched_id.to_string()))
}
} else {
Ok(None)
Err(SchedulerError::JobNotFound(sched_id.to_string()))
}
} else {
Err(SchedulerError::JobNotFound(sched_id.to_string()))
Err(SchedulerError::SchedulerInternalError(response.message))
}
}
@@ -1021,17 +1175,43 @@ mod tests {
}
#[test]
fn test_sessions_method_signature() {
// This test verifies the method signature is correct at compile time
// We just need to verify the method exists and can be called
fn test_job_status_detection_improvements() {
// Test that the new job status detection methods compile and work correctly
use tokio::runtime::Runtime;
// This will fail to compile if the method doesn't exist or has wrong signature
let _test_fn = |scheduler: &TemporalScheduler, id: &str, limit: usize| {
// This is a compile-time check - we don't actually call it
let _future = scheduler.sessions(id, limit);
};
let rt = Runtime::new().unwrap();
rt.block_on(async {
// This test verifies the improved job status detection compiles
match TemporalScheduler::new().await {
Ok(scheduler) => {
// Test the new status update method
match scheduler.update_job_status_from_sessions().await {
Ok(()) => {
println!("✅ update_job_status_from_sessions() works correctly");
}
Err(e) => {
println!("⚠️ update_job_status_from_sessions() returned error (expected if no jobs): {}", e);
}
}
println!("✅ sessions() method signature is correct");
// Test the improved get_running_job_info method
match scheduler.get_running_job_info("test-job").await {
Ok(None) => {
println!("✅ get_running_job_info() correctly returns None for non-existent job");
}
Ok(Some((session_id, start_time))) => {
println!("✅ get_running_job_info() returned session info: {} at {}", session_id, start_time);
}
Err(e) => {
println!("⚠️ get_running_job_info() returned error (expected): {}", e);
}
}
}
Err(e) => {
println!("⚠️ Temporal services not running - method signature test passed: {}", e);
}
}
});
}
#[test]
+1
View File
@@ -361,6 +361,7 @@ impl ScheduleToolTestBuilder {
paused: false,
current_session_id: None,
process_start_time: None,
execution_mode: Some("background".to_string()),
};
{
let mut jobs = self.scheduler.jobs.lock().await;