2025-06-19 18:59:47 -04:00
|
|
|
use git2::Repository;
|
2025-06-16 18:20:17 -04:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
2025-06-20 22:14:31 +01:00
|
|
|
use crate::app_state::AppState;
|
2025-06-16 18:20:17 -04:00
|
|
|
use crate::models::{
|
2025-06-20 22:39:06 +01:00
|
|
|
execution_process::{ExecutionProcess, ExecutionProcessStatus, ExecutionProcessType},
|
2025-06-17 22:09:10 -04:00
|
|
|
task::{Task, TaskStatus},
|
2025-06-16 23:13:33 -04:00
|
|
|
task_attempt::{TaskAttempt, TaskAttemptStatus},
|
|
|
|
|
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity},
|
2025-06-16 18:20:17 -04:00
|
|
|
};
|
|
|
|
|
|
2025-06-19 18:59:47 -04:00
|
|
|
/// Commit any unstaged changes in the worktree after execution completion
|
|
|
|
|
async fn commit_execution_changes(
|
|
|
|
|
worktree_path: &str,
|
|
|
|
|
attempt_id: Uuid,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
|
// Run git operations in a blocking task since git2 is synchronous
|
|
|
|
|
let worktree_path = worktree_path.to_string();
|
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
|
let worktree_repo = Repository::open(&worktree_path)?;
|
|
|
|
|
|
|
|
|
|
// Check if there are any changes to commit
|
|
|
|
|
let status = worktree_repo.statuses(None)?;
|
|
|
|
|
let has_changes = status.iter().any(|entry| {
|
|
|
|
|
let flags = entry.status();
|
|
|
|
|
flags.contains(git2::Status::INDEX_NEW)
|
|
|
|
|
|| flags.contains(git2::Status::INDEX_MODIFIED)
|
|
|
|
|
|| flags.contains(git2::Status::INDEX_DELETED)
|
|
|
|
|
|| flags.contains(git2::Status::WT_NEW)
|
|
|
|
|
|| flags.contains(git2::Status::WT_MODIFIED)
|
|
|
|
|
|| flags.contains(git2::Status::WT_DELETED)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if !has_changes {
|
|
|
|
|
return Ok::<(), Box<dyn std::error::Error + Send + Sync>>(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the current signature for commits
|
|
|
|
|
let signature = worktree_repo.signature()?;
|
|
|
|
|
|
|
|
|
|
// Get the current HEAD commit
|
|
|
|
|
let head = worktree_repo.head()?;
|
|
|
|
|
let parent_commit = head.peel_to_commit()?;
|
|
|
|
|
|
|
|
|
|
// Stage all changes
|
|
|
|
|
let mut worktree_index = worktree_repo.index()?;
|
|
|
|
|
worktree_index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
|
|
|
|
|
worktree_index.write()?;
|
|
|
|
|
|
|
|
|
|
let tree_id = worktree_index.write_tree()?;
|
|
|
|
|
let tree = worktree_repo.find_tree(tree_id)?;
|
|
|
|
|
|
|
|
|
|
// Create commit for the changes
|
|
|
|
|
let commit_message = format!("Task attempt {} - Final changes", attempt_id);
|
|
|
|
|
worktree_repo.commit(
|
|
|
|
|
Some("HEAD"),
|
|
|
|
|
&signature,
|
|
|
|
|
&signature,
|
|
|
|
|
&commit_message,
|
|
|
|
|
&tree,
|
|
|
|
|
&[&parent_commit],
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
})
|
|
|
|
|
.await??;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-19 21:18:18 -04:00
|
|
|
/// Play a system sound notification
|
2025-06-24 10:50:20 +01:00
|
|
|
async fn play_sound_notification(sound_file: &crate::models::config::SoundFile) {
|
2025-06-19 21:18:18 -04:00
|
|
|
// Use platform-specific sound notification
|
|
|
|
|
if cfg!(target_os = "macos") {
|
2025-06-24 10:50:20 +01:00
|
|
|
let sound_path = sound_file.to_path();
|
2025-06-19 21:18:18 -04:00
|
|
|
let _ = tokio::process::Command::new("afplay")
|
2025-06-24 10:50:20 +01:00
|
|
|
.arg(sound_path)
|
2025-06-19 21:18:18 -04:00
|
|
|
.spawn();
|
|
|
|
|
} else if cfg!(target_os = "linux") {
|
|
|
|
|
// Try different Linux notification sounds
|
2025-06-24 10:50:20 +01:00
|
|
|
let sound_path = sound_file.to_path();
|
2025-06-19 21:18:18 -04:00
|
|
|
if let Ok(_) = tokio::process::Command::new("paplay")
|
2025-06-24 10:50:20 +01:00
|
|
|
.arg(&sound_path)
|
2025-06-19 21:18:18 -04:00
|
|
|
.spawn()
|
|
|
|
|
{
|
|
|
|
|
// Success with paplay
|
|
|
|
|
} else if let Ok(_) = tokio::process::Command::new("aplay")
|
2025-06-24 10:50:20 +01:00
|
|
|
.arg(&sound_path)
|
2025-06-19 21:18:18 -04:00
|
|
|
.spawn()
|
|
|
|
|
{
|
|
|
|
|
// Success with aplay
|
|
|
|
|
} else {
|
|
|
|
|
// Try system bell as fallback
|
|
|
|
|
let _ = tokio::process::Command::new("echo")
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg("\\a")
|
|
|
|
|
.spawn();
|
|
|
|
|
}
|
|
|
|
|
} else if cfg!(target_os = "windows") {
|
|
|
|
|
let _ = tokio::process::Command::new("powershell")
|
|
|
|
|
.arg("-c")
|
|
|
|
|
.arg("[console]::beep(800, 300)")
|
|
|
|
|
.spawn();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-21 23:28:24 +01:00
|
|
|
/// Send a macOS push notification
|
|
|
|
|
async fn send_push_notification(title: &str, message: &str) {
|
|
|
|
|
if cfg!(target_os = "macos") {
|
|
|
|
|
let script = format!(
|
|
|
|
|
r#"display notification "{message}" with title "{title}" sound name "Glass""#,
|
|
|
|
|
message = message.replace('"', r#"\""#),
|
|
|
|
|
title = title.replace('"', r#"\""#)
|
|
|
|
|
);
|
2025-06-22 22:33:46 +01:00
|
|
|
|
2025-06-21 23:28:24 +01:00
|
|
|
let _ = tokio::process::Command::new("osascript")
|
|
|
|
|
.arg("-e")
|
|
|
|
|
.arg(script)
|
|
|
|
|
.spawn();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-16 18:20:17 -04:00
|
|
|
pub async fn execution_monitor(app_state: AppState) {
|
|
|
|
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
|
2025-06-16 23:13:33 -04:00
|
|
|
|
2025-06-16 18:20:17 -04:00
|
|
|
loop {
|
|
|
|
|
interval.tick().await;
|
2025-06-16 23:13:33 -04:00
|
|
|
|
2025-06-21 20:09:41 +01:00
|
|
|
// Check for completed processes FIRST to avoid race conditions
|
2025-06-20 21:46:28 +01:00
|
|
|
let completed_executions = app_state.get_running_executions_for_monitor().await;
|
2025-06-16 18:20:17 -04:00
|
|
|
|
|
|
|
|
// Handle completed executions
|
2025-06-21 19:31:41 +01:00
|
|
|
for (execution_process_id, task_attempt_id, success, exit_code) in completed_executions {
|
2025-06-16 23:13:33 -04:00
|
|
|
let status_text = if success {
|
|
|
|
|
"completed successfully"
|
|
|
|
|
} else {
|
|
|
|
|
"failed"
|
|
|
|
|
};
|
2025-06-16 18:20:17 -04:00
|
|
|
let exit_text = if let Some(code) = exit_code {
|
|
|
|
|
format!(" with exit code {}", code)
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
2025-06-16 23:13:33 -04:00
|
|
|
|
2025-06-21 19:31:41 +01:00
|
|
|
tracing::info!(
|
|
|
|
|
"Execution {} {}{}",
|
|
|
|
|
execution_process_id,
|
|
|
|
|
status_text,
|
|
|
|
|
exit_text
|
|
|
|
|
);
|
2025-06-16 18:20:17 -04:00
|
|
|
|
2025-06-20 22:39:06 +01:00
|
|
|
// Update the execution process record
|
|
|
|
|
let execution_status = if success {
|
|
|
|
|
ExecutionProcessStatus::Completed
|
|
|
|
|
} else {
|
|
|
|
|
ExecutionProcessStatus::Failed
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) = ExecutionProcess::update_completion(
|
|
|
|
|
&app_state.db_pool,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
execution_status,
|
|
|
|
|
exit_code,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to update execution process {} completion: {}",
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the execution process to determine next steps
|
|
|
|
|
if let Ok(Some(execution_process)) =
|
2025-06-21 19:31:41 +01:00
|
|
|
ExecutionProcess::find_by_id(&app_state.db_pool, execution_process_id).await
|
2025-06-20 22:39:06 +01:00
|
|
|
{
|
|
|
|
|
match execution_process.process_type {
|
|
|
|
|
ExecutionProcessType::SetupScript => {
|
|
|
|
|
handle_setup_completion(
|
|
|
|
|
&app_state,
|
|
|
|
|
task_attempt_id,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
execution_process,
|
|
|
|
|
success,
|
|
|
|
|
exit_code,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
ExecutionProcessType::CodingAgent => {
|
|
|
|
|
handle_coding_agent_completion(
|
|
|
|
|
&app_state,
|
|
|
|
|
task_attempt_id,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
execution_process,
|
|
|
|
|
success,
|
|
|
|
|
exit_code,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
ExecutionProcessType::DevServer => {
|
|
|
|
|
handle_dev_server_completion(
|
|
|
|
|
&app_state,
|
|
|
|
|
task_attempt_id,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
execution_process,
|
|
|
|
|
success,
|
|
|
|
|
exit_code,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to find execution process {} for completion handling",
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id
|
2025-06-20 22:39:06 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-21 20:09:41 +01:00
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
// Check for orphaned execution processes AFTER handling completions
|
2025-06-21 20:09:41 +01:00
|
|
|
// Add a small delay to ensure completed processes are properly handled first
|
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
let running_processes = match ExecutionProcess::find_running(&app_state.db_pool).await {
|
|
|
|
|
Ok(processes) => processes,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::error!("Failed to query running execution processes: {}", e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for process in running_processes {
|
|
|
|
|
// Additional check: if the process was recently updated, skip it
|
|
|
|
|
// This prevents race conditions with recent completions
|
|
|
|
|
let now = chrono::Utc::now();
|
|
|
|
|
let time_since_update = now - process.updated_at;
|
|
|
|
|
if time_since_update.num_seconds() < 10 {
|
|
|
|
|
// Process was updated within last 10 seconds, likely just completed
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
"Skipping recently updated process {} (updated {} seconds ago)",
|
|
|
|
|
process.id,
|
|
|
|
|
time_since_update.num_seconds()
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if this process is not actually running in the app state
|
|
|
|
|
if !app_state
|
|
|
|
|
.has_running_execution(process.task_attempt_id)
|
2025-06-22 22:33:46 +01:00
|
|
|
.await
|
2025-06-21 20:09:41 +01:00
|
|
|
{
|
2025-06-24 16:50:58 +01:00
|
|
|
// This is truly an orphaned execution process - mark it as failed
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Found orphaned execution process {} for task attempt {}",
|
|
|
|
|
process.id,
|
|
|
|
|
process.task_attempt_id
|
|
|
|
|
);
|
2025-06-21 20:09:41 +01:00
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
// Update the execution process status first
|
|
|
|
|
if let Err(e) = ExecutionProcess::update_completion(
|
2025-06-21 20:09:41 +01:00
|
|
|
&app_state.db_pool,
|
2025-06-24 16:50:58 +01:00
|
|
|
process.id,
|
|
|
|
|
ExecutionProcessStatus::Failed,
|
|
|
|
|
None, // No exit code for orphaned processes
|
2025-06-21 20:09:41 +01:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!(
|
2025-06-24 16:50:58 +01:00
|
|
|
"Failed to update orphaned execution process {} status: {}",
|
|
|
|
|
process.id,
|
2025-06-21 20:09:41 +01:00
|
|
|
e
|
|
|
|
|
);
|
2025-06-24 16:50:58 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
2025-06-21 20:09:41 +01:00
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
// Create task attempt activity for non-dev server processes
|
|
|
|
|
if process.process_type != ExecutionProcessType::DevServer {
|
|
|
|
|
let activity_id = Uuid::new_v4();
|
|
|
|
|
let create_activity = CreateTaskAttemptActivity {
|
|
|
|
|
execution_process_id: process.id,
|
|
|
|
|
status: Some(TaskAttemptStatus::ExecutorFailed),
|
|
|
|
|
note: Some("Execution lost (server restart or crash)".to_string()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) = TaskAttemptActivity::create(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
&create_activity,
|
|
|
|
|
activity_id,
|
|
|
|
|
TaskAttemptStatus::ExecutorFailed,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to create failed activity for orphaned process: {}",
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!("Marked orphaned execution process {} as failed", process.id);
|
|
|
|
|
|
|
|
|
|
// Update task status to InReview for coding agent and setup script failures
|
|
|
|
|
if matches!(
|
|
|
|
|
process.process_type,
|
|
|
|
|
ExecutionProcessType::CodingAgent | ExecutionProcessType::SetupScript
|
|
|
|
|
) {
|
2025-06-21 20:09:41 +01:00
|
|
|
if let Ok(Some(task_attempt)) =
|
2025-06-24 16:50:58 +01:00
|
|
|
TaskAttempt::find_by_id(&app_state.db_pool, process.task_attempt_id).await
|
2025-06-21 20:09:41 +01:00
|
|
|
{
|
|
|
|
|
if let Ok(Some(task)) =
|
|
|
|
|
Task::find_by_id(&app_state.db_pool, task_attempt.task_id).await
|
|
|
|
|
{
|
|
|
|
|
if let Err(e) = Task::update_status(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
task.id,
|
|
|
|
|
task.project_id,
|
|
|
|
|
TaskStatus::InReview,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!("Failed to update task status to InReview for orphaned attempt: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Handle setup script completion
|
|
|
|
|
async fn handle_setup_completion(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
task_attempt_id: Uuid,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id: Uuid,
|
2025-06-20 22:39:06 +01:00
|
|
|
_execution_process: ExecutionProcess,
|
|
|
|
|
success: bool,
|
|
|
|
|
exit_code: Option<i64>,
|
|
|
|
|
) {
|
|
|
|
|
let exit_text = if let Some(code) = exit_code {
|
|
|
|
|
format!(" with exit code {}", code)
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if success {
|
|
|
|
|
// Setup completed successfully, create activity
|
|
|
|
|
let activity_id = Uuid::new_v4();
|
|
|
|
|
let create_activity = CreateTaskAttemptActivity {
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
status: Some(TaskAttemptStatus::SetupComplete),
|
|
|
|
|
note: Some(format!("Setup script completed successfully{}", exit_text)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) = TaskAttemptActivity::create(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
&create_activity,
|
|
|
|
|
activity_id,
|
|
|
|
|
TaskAttemptStatus::SetupComplete,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!("Failed to create setup complete activity: {}", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get task and project info to start coding agent
|
|
|
|
|
if let Ok(Some(task_attempt)) =
|
|
|
|
|
TaskAttempt::find_by_id(&app_state.db_pool, task_attempt_id).await
|
|
|
|
|
{
|
|
|
|
|
if let Ok(Some(task)) = Task::find_by_id(&app_state.db_pool, task_attempt.task_id).await
|
|
|
|
|
{
|
|
|
|
|
// Start the coding agent
|
|
|
|
|
if let Err(e) = TaskAttempt::start_coding_agent(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
app_state,
|
|
|
|
|
task_attempt_id,
|
|
|
|
|
task.id,
|
|
|
|
|
task.project_id,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!("Failed to start coding agent after setup completion: {}", e);
|
|
|
|
|
}
|
2025-06-19 21:18:18 -04:00
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Setup failed, create activity and update task status
|
|
|
|
|
let activity_id = Uuid::new_v4();
|
|
|
|
|
let create_activity = CreateTaskAttemptActivity {
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
status: Some(TaskAttemptStatus::SetupFailed),
|
|
|
|
|
note: Some(format!("Setup script failed{}", exit_text)),
|
|
|
|
|
};
|
2025-06-19 21:18:18 -04:00
|
|
|
|
2025-06-20 22:39:06 +01:00
|
|
|
if let Err(e) = TaskAttemptActivity::create(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
&create_activity,
|
|
|
|
|
activity_id,
|
|
|
|
|
TaskAttemptStatus::SetupFailed,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!("Failed to create setup failed activity: {}", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update task status to InReview since setup failed
|
|
|
|
|
if let Ok(Some(task_attempt)) =
|
|
|
|
|
TaskAttempt::find_by_id(&app_state.db_pool, task_attempt_id).await
|
|
|
|
|
{
|
|
|
|
|
if let Ok(Some(task)) = Task::find_by_id(&app_state.db_pool, task_attempt.task_id).await
|
2025-06-16 23:13:33 -04:00
|
|
|
{
|
2025-06-20 22:39:06 +01:00
|
|
|
if let Err(e) = Task::update_status(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
task.id,
|
|
|
|
|
task.project_id,
|
|
|
|
|
TaskStatus::InReview,
|
|
|
|
|
)
|
|
|
|
|
.await
|
2025-06-19 18:59:47 -04:00
|
|
|
{
|
|
|
|
|
tracing::error!(
|
2025-06-20 22:39:06 +01:00
|
|
|
"Failed to update task status to InReview after setup failure: {}",
|
2025-06-19 18:59:47 -04:00
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-19 18:59:47 -04:00
|
|
|
|
2025-06-20 22:39:06 +01:00
|
|
|
/// Handle coding agent completion
|
|
|
|
|
async fn handle_coding_agent_completion(
|
|
|
|
|
app_state: &AppState,
|
|
|
|
|
task_attempt_id: Uuid,
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id: Uuid,
|
2025-06-20 22:39:06 +01:00
|
|
|
_execution_process: ExecutionProcess,
|
|
|
|
|
success: bool,
|
|
|
|
|
exit_code: Option<i64>,
|
|
|
|
|
) {
|
|
|
|
|
let exit_text = if let Some(code) = exit_code {
|
|
|
|
|
format!(" with exit code {}", code)
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
2025-06-17 22:09:10 -04:00
|
|
|
|
2025-06-20 22:39:06 +01:00
|
|
|
// Play sound notification if enabled
|
|
|
|
|
if app_state.get_sound_alerts_enabled().await {
|
2025-06-24 10:50:20 +01:00
|
|
|
let sound_file = app_state.get_sound_file().await;
|
|
|
|
|
play_sound_notification(&sound_file).await;
|
2025-06-20 22:39:06 +01:00
|
|
|
}
|
|
|
|
|
|
2025-06-21 23:28:24 +01:00
|
|
|
// Send push notification if enabled
|
|
|
|
|
if app_state.get_push_notifications_enabled().await {
|
|
|
|
|
let notification_title = "Task Complete";
|
|
|
|
|
let notification_message = if success {
|
|
|
|
|
"Task execution completed successfully"
|
|
|
|
|
} else {
|
|
|
|
|
"Task execution failed"
|
|
|
|
|
};
|
|
|
|
|
send_push_notification(notification_title, notification_message).await;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-20 22:39:06 +01:00
|
|
|
// Get task attempt to access worktree path for committing changes
|
|
|
|
|
if let Ok(Some(task_attempt)) =
|
|
|
|
|
TaskAttempt::find_by_id(&app_state.db_pool, task_attempt_id).await
|
|
|
|
|
{
|
|
|
|
|
// Commit any unstaged changes after execution completion
|
|
|
|
|
if let Err(e) = commit_execution_changes(&task_attempt.worktree_path, task_attempt_id).await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to commit execution changes for attempt {}: {}",
|
|
|
|
|
task_attempt_id,
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Successfully committed execution changes for attempt {}",
|
|
|
|
|
task_attempt_id
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create task attempt activity with appropriate completion status
|
|
|
|
|
let activity_id = Uuid::new_v4();
|
|
|
|
|
let status = if success {
|
|
|
|
|
TaskAttemptStatus::ExecutorComplete
|
|
|
|
|
} else {
|
|
|
|
|
TaskAttemptStatus::ExecutorFailed
|
|
|
|
|
};
|
|
|
|
|
let create_activity = CreateTaskAttemptActivity {
|
2025-06-21 19:31:41 +01:00
|
|
|
execution_process_id,
|
2025-06-20 22:39:06 +01:00
|
|
|
status: Some(status.clone()),
|
|
|
|
|
note: Some(format!("Coding agent execution completed{}", exit_text)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) =
|
|
|
|
|
TaskAttemptActivity::create(&app_state.db_pool, &create_activity, activity_id, status)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!("Failed to create executor completion activity: {}", e);
|
|
|
|
|
} else {
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Task attempt {} set to paused after coding agent completion",
|
|
|
|
|
task_attempt_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get task to access task_id and project_id for status update
|
|
|
|
|
if let Ok(Some(task)) = Task::find_by_id(&app_state.db_pool, task_attempt.task_id).await
|
|
|
|
|
{
|
|
|
|
|
// Update task status to InReview
|
|
|
|
|
if let Err(e) = Task::update_status(
|
2025-06-19 18:59:47 -04:00
|
|
|
&app_state.db_pool,
|
2025-06-20 22:39:06 +01:00
|
|
|
task.id,
|
|
|
|
|
task.project_id,
|
|
|
|
|
TaskStatus::InReview,
|
2025-06-19 18:59:47 -04:00
|
|
|
)
|
|
|
|
|
.await
|
2025-06-17 22:09:10 -04:00
|
|
|
{
|
2025-06-20 22:39:06 +01:00
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to update task status to InReview for completed attempt: {}",
|
|
|
|
|
e
|
2025-06-19 18:59:47 -04:00
|
|
|
);
|
2025-06-17 22:09:10 -04:00
|
|
|
}
|
2025-06-16 18:20:17 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
} else {
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to find task attempt {} for coding agent completion",
|
|
|
|
|
task_attempt_id
|
|
|
|
|
);
|
2025-06-16 18:20:17 -04:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
|
|
|
|
|
/// Handle dev server completion (future functionality)
|
|
|
|
|
async fn handle_dev_server_completion(
|
2025-06-24 16:50:58 +01:00
|
|
|
app_state: &AppState,
|
2025-06-20 22:39:06 +01:00
|
|
|
task_attempt_id: Uuid,
|
2025-06-24 16:50:58 +01:00
|
|
|
execution_process_id: Uuid,
|
2025-06-20 22:39:06 +01:00
|
|
|
_execution_process: ExecutionProcess,
|
2025-06-24 16:50:58 +01:00
|
|
|
success: bool,
|
2025-06-20 22:39:06 +01:00
|
|
|
exit_code: Option<i64>,
|
|
|
|
|
) {
|
|
|
|
|
let exit_text = if let Some(code) = exit_code {
|
|
|
|
|
format!(" with exit code {}", code)
|
|
|
|
|
} else {
|
|
|
|
|
String::new()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
"Dev server for task attempt {} completed{}",
|
|
|
|
|
task_attempt_id,
|
|
|
|
|
exit_text
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-24 16:50:58 +01:00
|
|
|
// Update execution process status instead of creating activity
|
|
|
|
|
let process_status = if success {
|
|
|
|
|
ExecutionProcessStatus::Completed
|
|
|
|
|
} else {
|
|
|
|
|
ExecutionProcessStatus::Failed
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Err(e) = ExecutionProcess::update_completion(
|
|
|
|
|
&app_state.db_pool,
|
|
|
|
|
execution_process_id,
|
|
|
|
|
process_status,
|
|
|
|
|
exit_code,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
tracing::error!(
|
|
|
|
|
"Failed to update dev server execution process status: {}",
|
|
|
|
|
e
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-06-20 22:39:06 +01:00
|
|
|
}
|