Remove task attempt activity (vibe-kanban) (#242)
* ## Summary I have successfully completed the task to remove the task attempt activity functionality from the codebase. Here's what was accomplished: ### ✅ **Completed Work:** 1. **Database Migration**: Created `backend/migrations/20250717000000_drop_task_attempt_activities.sql` to drop the `task_attempt_activities` table and its indexes 2. **Backend Cleanup**: - Deleted `backend/src/models/task_attempt_activity.rs` model file - Removed imports and exports from `backend/src/models/mod.rs` - Cleaned up all references in `backend/src/routes/task_attempts.rs`: - Removed `get_task_attempt_activities` and `create_task_attempt_activity` functions - Removed the API route endpoints - Removed activity creation calls in process stop handlers - Updated `backend/src/services/process_service.rs` to remove activity creation - Updated `backend/src/execution_monitor.rs` to remove activity creation throughout the execution flow - Removed type exports from `backend/src/bin/generate_types.rs` 3. **Frontend Cleanup**: - Regenerated TypeScript types (TaskAttemptActivity types automatically removed) - Removed `getActivities` API function from `frontend/src/lib/api.ts` - Deleted `frontend/src/components/tasks/TaskActivityHistory.tsx` component - Updated `frontend/src/lib/types.ts` to remove activity references - Updated `frontend/src/components/context/TaskDetailsContextProvider.tsx` to fetch only processes (no activities) - Updated `frontend/src/components/tasks/TaskFollowUpSection.tsx` to determine follow-up eligibility based on completed coding agent processes instead of activities 4. **Build Verification**: Both backend (Rust) and frontend (TypeScript) build successfully ### ⚠️ **Remaining Issue:** There's one SQL query in `backend/src/models/task.rs` that still references the `task_attempt_activities` table. The query has become malformed during the editing process. This needs to be fixed to complete the task. The functionality has been successfully removed with no compilation errors in the main application code. The only remaining work is to fix the SQL query that determines task status based on execution process states rather than activities. * update queries * clippy * prettier * mark task as failed or killed * sql rebase fixes
This commit is contained in:
committed by
GitHub
parent
ad38c8af53
commit
0bdb2840c0
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO task_attempt_activities (id, execution_process_id, status, note) \n VALUES ($1, $2, $3, $4) \n RETURNING id as \"id!: Uuid\", execution_process_id as \"execution_process_id!: Uuid\", status as \"status!: TaskAttemptStatus\", note, created_at as \"created_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "execution_process_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: TaskAttemptStatus",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "52293d5438887ad86a1416abe78a1e68424426af5bf29db3afdafb6202ca015f"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
86
backend/.sqlx/query-94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a.json
generated
Normal file
86
backend/.sqlx/query-94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a.json
generated
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT \n t.id AS \"id!: Uuid\",\n t.project_id AS \"project_id!: Uuid\",\n t.title,\n t.description,\n t.status AS \"status!: TaskStatus\",\n t.parent_task_attempt AS \"parent_task_attempt: Uuid\", \n t.created_at AS \"created_at!: DateTime<Utc>\",\n t.updated_at AS \"updated_at!: DateTime<Utc>\",\n CASE \n WHEN ip.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_in_progress_attempt!: i64\",\n CASE \n WHEN ma.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_merged_attempt!: i64\",\n CASE \n WHEN fa.task_id IS NOT NULL THEN true \n ELSE false \n END AS \"has_failed_attempt!: i64\",\n latest_executor_attempts.executor AS \"latest_attempt_executor\"\n FROM tasks t\n\n -- in-progress if any running setupscript/codingagent\n LEFT JOIN (\n SELECT DISTINCT ta.task_id\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.status = 'running'\n AND ep.process_type IN ('setupscript','codingagent')\n ) ip \n ON t.id = ip.task_id\n\n -- merged if merge_commit not null\n LEFT JOIN (\n SELECT DISTINCT task_id\n FROM task_attempts\n WHERE merge_commit IS NOT NULL\n ) ma \n ON t.id = ma.task_id\n\n -- failed if latest attempt has a failed setupscript/codingagent\n LEFT JOIN (\n SELECT sub.task_id\n FROM (\n SELECT\n ta.task_id,\n ep.status,\n ep.process_type,\n ROW_NUMBER() OVER (\n PARTITION BY ta.task_id \n ORDER BY ta.created_at DESC\n ) AS rn\n FROM task_attempts ta\n JOIN execution_processes ep \n ON ta.id = ep.task_attempt_id\n WHERE ep.process_type IN ('setupscript','codingagent')\n ) sub\n WHERE sub.rn = 1\n AND sub.status IN ('failed','killed')\n ) fa\n ON t.id = fa.task_id\n\n -- get the executor of the latest attempt\n LEFT JOIN (\n SELECT task_id, executor\n FROM (\n SELECT task_id, executor, created_at,\n ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn\n FROM task_attempts\n ) latest_attempts\n WHERE rn = 1\n ) latest_executor_attempts \n ON t.id = latest_executor_attempts.task_id\n\n WHERE t.project_id = $1\n ORDER BY t.created_at DESC;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: TaskStatus",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "parent_task_attempt: Uuid",
|
||||
"ordinal": 5,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "has_in_progress_attempt!: i64",
|
||||
"ordinal": 8,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "has_merged_attempt!: i64",
|
||||
"ordinal": 9,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "has_failed_attempt!: i64",
|
||||
"ordinal": 10,
|
||||
"type_info": "Integer"
|
||||
},
|
||||
{
|
||||
"name": "latest_attempt_executor",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "94a21be956c9451a8b117d25ffd4e5ee75bba0aa032139572cf87651e2856f3a"
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT DISTINCT ep.id as \"id!: Uuid\"\n FROM execution_processes ep\n INNER JOIN (\n SELECT execution_process_id, MAX(created_at) as latest_created_at\n FROM task_attempt_activities\n GROUP BY execution_process_id\n ) latest_activity ON ep.id = latest_activity.execution_process_id\n INNER JOIN task_attempt_activities taa ON ep.id = taa.execution_process_id \n AND taa.created_at = latest_activity.latest_created_at\n WHERE taa.status IN ($1, $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "b029d6c1a0ccbd90e54c0e948dbb5623c650f935918e65ec34644210be44c0b1"
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT \n taa.id as \"activity_id!: Uuid\",\n taa.execution_process_id as \"execution_process_id!: Uuid\",\n taa.status as \"status!: TaskAttemptStatus\",\n taa.note,\n taa.created_at as \"created_at!: DateTime<Utc>\",\n es.prompt\n FROM task_attempt_activities taa\n INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id\n LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id\n WHERE ep.task_attempt_id = $1\n ORDER BY taa.created_at ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "activity_id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "execution_process_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: TaskAttemptStatus",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "prompt",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "bd57e47989627ac5e9f44e83f4bacff590d357586b149c7c17ff0e8f349e7025"
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", execution_process_id as \"execution_process_id!: Uuid\", status as \"status!: TaskAttemptStatus\", note, created_at as \"created_at!: DateTime<Utc>\"\n FROM task_attempt_activities \n WHERE execution_process_id = $1 \n ORDER BY created_at DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "execution_process_id!: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "status!: TaskAttemptStatus",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "note",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "cb56bae2e03ef702cd8737f072d9c73957cc04695741fd7788370dfd9184548a"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Migration to drop task_attempt_activities table
|
||||
-- This removes the task attempt activity tracking functionality
|
||||
|
||||
-- Drop indexes first
|
||||
DROP INDEX IF EXISTS idx_task_attempt_activities_execution_process_id;
|
||||
DROP INDEX IF EXISTS idx_task_attempt_activities_created_at;
|
||||
|
||||
-- Drop the table
|
||||
DROP TABLE IF EXISTS task_attempt_activities;
|
||||
@@ -108,9 +108,6 @@ fn generate_types_content() -> String {
|
||||
vibe_kanban::models::task_attempt::CreateTaskAttempt::decl(),
|
||||
vibe_kanban::models::task_attempt::UpdateTaskAttempt::decl(),
|
||||
vibe_kanban::models::task_attempt::CreateFollowUpAttempt::decl(),
|
||||
vibe_kanban::models::task_attempt_activity::TaskAttemptActivity::decl(),
|
||||
vibe_kanban::models::task_attempt_activity::TaskAttemptActivityWithPrompt::decl(),
|
||||
vibe_kanban::models::task_attempt_activity::CreateTaskAttemptActivity::decl(),
|
||||
vibe_kanban::routes::filesystem::DirectoryEntry::decl(),
|
||||
vibe_kanban::routes::filesystem::DirectoryListResponse::decl(),
|
||||
vibe_kanban::routes::auth::DeviceStartResponse::decl(),
|
||||
|
||||
@@ -6,8 +6,7 @@ use crate::{
|
||||
models::{
|
||||
execution_process::{ExecutionProcess, ExecutionProcessStatus, ExecutionProcessType},
|
||||
task::{Task, TaskStatus},
|
||||
task_attempt::{TaskAttempt, TaskAttemptStatus},
|
||||
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity},
|
||||
task_attempt::TaskAttempt,
|
||||
},
|
||||
services::{NotificationConfig, NotificationService, ProcessService},
|
||||
utils::worktree_manager::WorktreeManager,
|
||||
@@ -573,10 +572,8 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
handle_setup_completion(
|
||||
&app_state,
|
||||
task_attempt_id,
|
||||
execution_process_id,
|
||||
execution_process,
|
||||
success,
|
||||
exit_code,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -669,30 +666,7 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Process marked as failed
|
||||
|
||||
tracing::info!("Marked orphaned execution process {} as failed", process.id);
|
||||
|
||||
@@ -766,17 +740,9 @@ pub async fn execution_monitor(app_state: AppState) {
|
||||
async fn handle_setup_completion(
|
||||
app_state: &AppState,
|
||||
task_attempt_id: Uuid,
|
||||
execution_process_id: Uuid,
|
||||
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 {
|
||||
// Mark setup as completed in database
|
||||
if let Err(e) = TaskAttempt::mark_setup_completed(&app_state.db_pool, task_attempt_id).await
|
||||
@@ -788,25 +754,7 @@ async fn handle_setup_completion(
|
||||
);
|
||||
}
|
||||
|
||||
// Setup completed successfully, create activity
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id,
|
||||
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;
|
||||
}
|
||||
// Setup completed successfully
|
||||
|
||||
// Check for delegation context in process args
|
||||
let delegation_result = if let Some(args_json) = &execution_process.args {
|
||||
@@ -845,24 +793,7 @@ async fn handle_setup_completion(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Setup failed, create activity and update task status
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id,
|
||||
status: Some(TaskAttemptStatus::SetupFailed),
|
||||
note: Some(format!("Setup script failed{}", exit_text)),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
// Setup failed, update task status
|
||||
|
||||
// Update task status to InReview since setup failed
|
||||
if let Ok(Some(task_attempt)) =
|
||||
@@ -897,12 +828,6 @@ async fn handle_coding_agent_completion(
|
||||
success: bool,
|
||||
exit_code: Option<i64>,
|
||||
) {
|
||||
let exit_text = if let Some(code) = exit_code {
|
||||
format!(" with exit code {}", code)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Extract and store assistant message from execution logs
|
||||
let summary = if let Some(stdout) = &execution_process.stdout {
|
||||
if let Some(assistant_message) = crate::executor::parse_assistant_message_from_logs(stdout)
|
||||
@@ -1020,60 +945,40 @@ async fn handle_coding_agent_completion(
|
||||
);
|
||||
}
|
||||
|
||||
// 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 {
|
||||
execution_process_id,
|
||||
status: Some(status.clone()),
|
||||
note: Some(format!("Coding agent execution completed{}", exit_text)),
|
||||
};
|
||||
// Coding agent execution completed
|
||||
tracing::info!(
|
||||
"Task attempt {} set to paused after coding agent completion",
|
||||
task_attempt_id
|
||||
);
|
||||
|
||||
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
|
||||
{
|
||||
app_state
|
||||
.track_analytics_event(
|
||||
"task_attempt_finished",
|
||||
Some(serde_json::json!({
|
||||
"task_id": task.id.to_string(),
|
||||
"project_id": task.project_id.to_string(),
|
||||
"attempt_id": task_attempt_id.to_string(),
|
||||
"execution_success": success,
|
||||
"exit_code": exit_code,
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Update task status to InReview
|
||||
if let Err(e) = Task::update_status(
|
||||
&app_state.db_pool,
|
||||
task.id,
|
||||
task.project_id,
|
||||
TaskStatus::InReview,
|
||||
// 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 {
|
||||
app_state
|
||||
.track_analytics_event(
|
||||
"task_attempt_finished",
|
||||
Some(serde_json::json!({
|
||||
"task_id": task.id.to_string(),
|
||||
"project_id": task.project_id.to_string(),
|
||||
"attempt_id": task_attempt_id.to_string(),
|
||||
"execution_success": success,
|
||||
"exit_code": exit_code,
|
||||
})),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to update task status to InReview for completed attempt: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
.await;
|
||||
|
||||
// Update task status to InReview
|
||||
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 completed attempt: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,7 @@ pub mod executor_session;
|
||||
pub mod project;
|
||||
pub mod task;
|
||||
pub mod task_attempt;
|
||||
pub mod task_attempt_activity;
|
||||
|
||||
pub mod task_template;
|
||||
|
||||
pub use api_response::ApiResponse;
|
||||
|
||||
@@ -81,114 +81,85 @@ impl Task {
|
||||
) -> Result<Vec<TaskWithAttemptStatus>, sqlx::Error> {
|
||||
let records = sqlx::query!(
|
||||
r#"SELECT
|
||||
t.id AS "id!: Uuid",
|
||||
t.project_id AS "project_id!: Uuid",
|
||||
t.title,
|
||||
t.description,
|
||||
t.status AS "status!: TaskStatus",
|
||||
t.parent_task_attempt AS "parent_task_attempt: Uuid",
|
||||
t.created_at AS "created_at!: DateTime<Utc>",
|
||||
t.updated_at AS "updated_at!: DateTime<Utc>",
|
||||
CASE
|
||||
WHEN in_progress_attempts.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_in_progress_attempt!: i64",
|
||||
CASE
|
||||
WHEN merged_attempts.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_merged_attempt!",
|
||||
CASE
|
||||
WHEN failed_attempts.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_failed_attempt!",
|
||||
latest_executor_attempts.executor AS "latest_attempt_executor"
|
||||
FROM tasks t
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ta.task_id
|
||||
t.id AS "id!: Uuid",
|
||||
t.project_id AS "project_id!: Uuid",
|
||||
t.title,
|
||||
t.description,
|
||||
t.status AS "status!: TaskStatus",
|
||||
t.parent_task_attempt AS "parent_task_attempt: Uuid",
|
||||
t.created_at AS "created_at!: DateTime<Utc>",
|
||||
t.updated_at AS "updated_at!: DateTime<Utc>",
|
||||
CASE
|
||||
WHEN ip.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_in_progress_attempt!: i64",
|
||||
CASE
|
||||
WHEN ma.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_merged_attempt!: i64",
|
||||
CASE
|
||||
WHEN fa.task_id IS NOT NULL THEN true
|
||||
ELSE false
|
||||
END AS "has_failed_attempt!: i64",
|
||||
latest_executor_attempts.executor AS "latest_attempt_executor"
|
||||
FROM tasks t
|
||||
|
||||
-- in-progress if any running setupscript/codingagent
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ta.task_id
|
||||
FROM task_attempts ta
|
||||
JOIN execution_processes ep
|
||||
ON ta.id = ep.task_attempt_id
|
||||
WHERE ep.status = 'running'
|
||||
AND ep.process_type IN ('setupscript','codingagent')
|
||||
) ip
|
||||
ON t.id = ip.task_id
|
||||
|
||||
-- merged if merge_commit not null
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT task_id
|
||||
FROM task_attempts
|
||||
WHERE merge_commit IS NOT NULL
|
||||
) ma
|
||||
ON t.id = ma.task_id
|
||||
|
||||
-- failed if latest attempt has a failed setupscript/codingagent
|
||||
LEFT JOIN (
|
||||
SELECT sub.task_id
|
||||
FROM (
|
||||
SELECT
|
||||
ta.task_id,
|
||||
ep.status,
|
||||
ep.process_type,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY ta.task_id
|
||||
ORDER BY ta.created_at DESC
|
||||
) AS rn
|
||||
FROM task_attempts ta
|
||||
JOIN execution_processes ep
|
||||
ON ta.id = ep.task_attempt_id
|
||||
JOIN (
|
||||
-- pick exactly one “latest” activity per process,
|
||||
-- tiebreaking so that running‐states are lower priority
|
||||
SELECT execution_process_id, status
|
||||
FROM (
|
||||
SELECT
|
||||
execution_process_id,
|
||||
status,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY execution_process_id
|
||||
ORDER BY
|
||||
created_at DESC,
|
||||
CASE
|
||||
WHEN status IN ('setuprunning','executorrunning') THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) AS rn
|
||||
FROM task_attempt_activities
|
||||
) sub
|
||||
WHERE rn = 1
|
||||
) latest_act
|
||||
ON ep.id = latest_act.execution_process_id
|
||||
WHERE latest_act.status IN ('setuprunning','executorrunning')
|
||||
) in_progress_attempts
|
||||
ON t.id = in_progress_attempts.task_id
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ta.task_id
|
||||
FROM task_attempts ta
|
||||
WHERE ta.merge_commit IS NOT NULL
|
||||
) merged_attempts
|
||||
ON t.id = merged_attempts.task_id
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT latest_attempts.task_id
|
||||
FROM (
|
||||
-- Get the latest attempt for each task
|
||||
SELECT task_id, id as attempt_id, created_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn
|
||||
FROM task_attempts
|
||||
WHERE merge_commit IS NULL -- Don't show as failed if already merged
|
||||
) latest_attempts
|
||||
JOIN execution_processes ep
|
||||
ON latest_attempts.attempt_id = ep.task_attempt_id
|
||||
JOIN (
|
||||
-- pick exactly one "latest" activity per process,
|
||||
-- tiebreaking so that running‐states are lower priority
|
||||
SELECT execution_process_id, status
|
||||
FROM (
|
||||
SELECT
|
||||
execution_process_id,
|
||||
status,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY execution_process_id
|
||||
ORDER BY
|
||||
created_at DESC,
|
||||
CASE
|
||||
WHEN status IN ('setuprunning','executorrunning') THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) AS rn
|
||||
FROM task_attempt_activities
|
||||
) sub
|
||||
WHERE rn = 1
|
||||
) latest_act
|
||||
ON ep.id = latest_act.execution_process_id
|
||||
WHERE latest_attempts.rn = 1 -- Only consider the latest attempt
|
||||
AND latest_act.status IN ('setupfailed','executorfailed')
|
||||
) failed_attempts
|
||||
ON t.id = failed_attempts.task_id
|
||||
LEFT JOIN (
|
||||
SELECT task_id, executor
|
||||
FROM (
|
||||
SELECT task_id, executor, created_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn
|
||||
FROM task_attempts
|
||||
) latest_attempts
|
||||
WHERE rn = 1
|
||||
) latest_executor_attempts
|
||||
ON t.id = latest_executor_attempts.task_id
|
||||
WHERE t.project_id = $1
|
||||
ORDER BY t.created_at DESC;
|
||||
"#,
|
||||
ON ta.id = ep.task_attempt_id
|
||||
WHERE ep.process_type IN ('setupscript','codingagent')
|
||||
) sub
|
||||
WHERE sub.rn = 1
|
||||
AND sub.status IN ('failed','killed')
|
||||
) fa
|
||||
ON t.id = fa.task_id
|
||||
|
||||
-- get the executor of the latest attempt
|
||||
LEFT JOIN (
|
||||
SELECT task_id, executor
|
||||
FROM (
|
||||
SELECT task_id, executor, created_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY task_id ORDER BY created_at DESC) AS rn
|
||||
FROM task_attempts
|
||||
) latest_attempts
|
||||
WHERE rn = 1
|
||||
) latest_executor_attempts
|
||||
ON t.id = latest_executor_attempts.task_id
|
||||
|
||||
WHERE t.project_id = $1
|
||||
ORDER BY t.created_at DESC;
|
||||
"#,
|
||||
project_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
@@ -196,19 +167,19 @@ impl Task {
|
||||
|
||||
let tasks = records
|
||||
.into_iter()
|
||||
.map(|record| TaskWithAttemptStatus {
|
||||
id: record.id,
|
||||
project_id: record.project_id,
|
||||
title: record.title,
|
||||
description: record.description,
|
||||
status: record.status,
|
||||
parent_task_attempt: record.parent_task_attempt,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
has_in_progress_attempt: record.has_in_progress_attempt != 0,
|
||||
has_merged_attempt: record.has_merged_attempt != 0,
|
||||
has_failed_attempt: record.has_failed_attempt != 0,
|
||||
latest_attempt_executor: record.latest_attempt_executor,
|
||||
.map(|rec| TaskWithAttemptStatus {
|
||||
id: rec.id,
|
||||
project_id: rec.project_id,
|
||||
title: rec.title,
|
||||
description: rec.description,
|
||||
status: rec.status,
|
||||
parent_task_attempt: rec.parent_task_attempt,
|
||||
created_at: rec.created_at,
|
||||
updated_at: rec.updated_at,
|
||||
has_in_progress_attempt: rec.has_in_progress_attempt != 0,
|
||||
has_merged_attempt: rec.has_merged_attempt != 0,
|
||||
has_failed_attempt: rec.has_failed_attempt != 0,
|
||||
latest_attempt_executor: rec.latest_attempt_executor,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::task_attempt::TaskAttemptStatus;
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct TaskAttemptActivity {
|
||||
pub id: Uuid,
|
||||
pub execution_process_id: Uuid, // Foreign key to ExecutionProcess
|
||||
pub status: TaskAttemptStatus,
|
||||
pub note: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct CreateTaskAttemptActivity {
|
||||
pub execution_process_id: Uuid,
|
||||
pub status: Option<TaskAttemptStatus>,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct TaskAttemptActivityWithPrompt {
|
||||
pub id: Uuid,
|
||||
pub execution_process_id: Uuid,
|
||||
pub status: TaskAttemptStatus,
|
||||
pub note: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub prompt: Option<String>, // From executor_session
|
||||
}
|
||||
|
||||
impl TaskAttemptActivity {
|
||||
#[allow(dead_code)]
|
||||
pub async fn find_by_execution_process_id(
|
||||
pool: &SqlitePool,
|
||||
execution_process_id: Uuid,
|
||||
) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
TaskAttemptActivity,
|
||||
r#"SELECT id as "id!: Uuid", execution_process_id as "execution_process_id!: Uuid", status as "status!: TaskAttemptStatus", note, created_at as "created_at!: DateTime<Utc>"
|
||||
FROM task_attempt_activities
|
||||
WHERE execution_process_id = $1
|
||||
ORDER BY created_at DESC"#,
|
||||
execution_process_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
pool: &SqlitePool,
|
||||
data: &CreateTaskAttemptActivity,
|
||||
activity_id: Uuid,
|
||||
status: TaskAttemptStatus,
|
||||
) -> Result<Self, sqlx::Error> {
|
||||
let status_value = status as TaskAttemptStatus;
|
||||
sqlx::query_as!(
|
||||
TaskAttemptActivity,
|
||||
r#"INSERT INTO task_attempt_activities (id, execution_process_id, status, note)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id as "id!: Uuid", execution_process_id as "execution_process_id!: Uuid", status as "status!: TaskAttemptStatus", note, created_at as "created_at!: DateTime<Utc>""#,
|
||||
activity_id,
|
||||
data.execution_process_id,
|
||||
status_value,
|
||||
data.note
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn find_processes_with_latest_running_status(
|
||||
pool: &SqlitePool,
|
||||
) -> Result<Vec<uuid::Uuid>, sqlx::Error> {
|
||||
let records = sqlx::query!(
|
||||
r#"SELECT DISTINCT ep.id as "id!: Uuid"
|
||||
FROM execution_processes ep
|
||||
INNER JOIN (
|
||||
SELECT execution_process_id, MAX(created_at) as latest_created_at
|
||||
FROM task_attempt_activities
|
||||
GROUP BY execution_process_id
|
||||
) latest_activity ON ep.id = latest_activity.execution_process_id
|
||||
INNER JOIN task_attempt_activities taa ON ep.id = taa.execution_process_id
|
||||
AND taa.created_at = latest_activity.latest_created_at
|
||||
WHERE taa.status IN ($1, $2)"#,
|
||||
TaskAttemptStatus::ExecutorRunning as TaskAttemptStatus,
|
||||
TaskAttemptStatus::SetupRunning as TaskAttemptStatus
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(records.into_iter().map(|r| r.id).collect())
|
||||
}
|
||||
|
||||
/// Find activities for all execution processes in a task attempt, with executor session prompts
|
||||
pub async fn find_with_prompts_by_task_attempt_id(
|
||||
pool: &SqlitePool,
|
||||
task_attempt_id: Uuid,
|
||||
) -> Result<Vec<TaskAttemptActivityWithPrompt>, sqlx::Error> {
|
||||
let records = sqlx::query!(
|
||||
r#"SELECT
|
||||
taa.id as "activity_id!: Uuid",
|
||||
taa.execution_process_id as "execution_process_id!: Uuid",
|
||||
taa.status as "status!: TaskAttemptStatus",
|
||||
taa.note,
|
||||
taa.created_at as "created_at!: DateTime<Utc>",
|
||||
es.prompt
|
||||
FROM task_attempt_activities taa
|
||||
INNER JOIN execution_processes ep ON taa.execution_process_id = ep.id
|
||||
LEFT JOIN executor_sessions es ON es.execution_process_id = ep.id
|
||||
WHERE ep.task_attempt_id = $1
|
||||
ORDER BY taa.created_at ASC"#,
|
||||
task_attempt_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(records
|
||||
.into_iter()
|
||||
.map(|record| TaskAttemptActivityWithPrompt {
|
||||
id: record.activity_id,
|
||||
execution_process_id: record.execution_process_id,
|
||||
status: record.status,
|
||||
note: record.note,
|
||||
created_at: record.created_at,
|
||||
prompt: record.prompt,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,7 @@ use crate::{
|
||||
task::{Task, TaskStatus},
|
||||
task_attempt::{
|
||||
BranchStatus, CreateFollowUpAttempt, CreatePrParams, CreateTaskAttempt, TaskAttempt,
|
||||
TaskAttemptState, TaskAttemptStatus, WorktreeDiff,
|
||||
},
|
||||
task_attempt_activity::{
|
||||
CreateTaskAttemptActivity, TaskAttemptActivity, TaskAttemptActivityWithPrompt,
|
||||
TaskAttemptState, WorktreeDiff,
|
||||
},
|
||||
ApiResponse,
|
||||
},
|
||||
@@ -76,40 +73,6 @@ pub async fn get_task_attempts(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_task_attempt_activities(
|
||||
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
|
||||
State(app_state): State<AppState>,
|
||||
) -> Result<ResponseJson<ApiResponse<Vec<TaskAttemptActivityWithPrompt>>>, StatusCode> {
|
||||
// Verify task attempt exists and belongs to the correct task
|
||||
match TaskAttempt::exists_for_task(&app_state.db_pool, attempt_id, task_id, project_id).await {
|
||||
Ok(false) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check task attempt existence: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
Ok(true) => {}
|
||||
}
|
||||
|
||||
// Get activities with prompts for the task attempt
|
||||
match TaskAttemptActivity::find_with_prompts_by_task_attempt_id(&app_state.db_pool, attempt_id)
|
||||
.await
|
||||
{
|
||||
Ok(activities) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(activities),
|
||||
message: None,
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to fetch task attempt activities for attempt {}: {}",
|
||||
attempt_id,
|
||||
e
|
||||
);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_task_attempt(
|
||||
Path((project_id, task_id)): Path<(Uuid, Uuid)>,
|
||||
State(app_state): State<AppState>,
|
||||
@@ -174,61 +137,6 @@ pub async fn create_task_attempt(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_task_attempt_activity(
|
||||
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
|
||||
State(app_state): State<AppState>,
|
||||
Json(payload): Json<CreateTaskAttemptActivity>,
|
||||
) -> Result<ResponseJson<ApiResponse<TaskAttemptActivity>>, StatusCode> {
|
||||
// Verify task attempt exists and belongs to the correct task
|
||||
match TaskAttempt::exists_for_task(&app_state.db_pool, attempt_id, task_id, project_id).await {
|
||||
Ok(false) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to check task attempt existence: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
Ok(true) => {}
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
// Check that execution_process_id is provided in payload
|
||||
if payload.execution_process_id == Uuid::nil() {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Verify the execution process exists and belongs to this task attempt
|
||||
match ExecutionProcess::find_by_id(&app_state.db_pool, payload.execution_process_id).await {
|
||||
Ok(Some(process)) => {
|
||||
if process.task_attempt_id != attempt_id {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to verify execution process: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// Default to SetupRunning status if not provided
|
||||
let status = payload
|
||||
.status
|
||||
.clone()
|
||||
.unwrap_or(TaskAttemptStatus::SetupRunning);
|
||||
|
||||
match TaskAttemptActivity::create(&app_state.db_pool, &payload, id, status).await {
|
||||
Ok(activity) => Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
data: Some(activity),
|
||||
message: Some("Task attempt activity created successfully".to_string()),
|
||||
})),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create task attempt activity: {}", e);
|
||||
Err(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_task_attempt_diff(
|
||||
Path((project_id, task_id, attempt_id)): Path<(Uuid, Uuid, Uuid)>,
|
||||
State(app_state): State<AppState>,
|
||||
@@ -736,36 +644,7 @@ pub async fn stop_all_execution_processes(
|
||||
tracing::error!("Failed to update execution process status: {}", e);
|
||||
errors.push(format!("Failed to update process {} status", process.id));
|
||||
} else {
|
||||
// Create activity record for stopped processes (skip dev servers)
|
||||
if !matches!(
|
||||
process.process_type,
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer
|
||||
) {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process.id,
|
||||
status: Some(TaskAttemptStatus::ExecutorFailed),
|
||||
note: Some(format!(
|
||||
"Execution process {:?} ({}) stopped by user",
|
||||
process.process_type, process.id
|
||||
)),
|
||||
};
|
||||
|
||||
if let Err(e) = TaskAttemptActivity::create(
|
||||
&app_state.db_pool,
|
||||
&create_activity,
|
||||
activity_id,
|
||||
TaskAttemptStatus::ExecutorFailed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to create stopped activity: {}", e);
|
||||
errors.push(format!(
|
||||
"Failed to create activity for process {}",
|
||||
process.id
|
||||
));
|
||||
}
|
||||
}
|
||||
// Process stopped successfully
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
@@ -824,7 +703,7 @@ pub async fn stop_execution_process(
|
||||
}
|
||||
|
||||
// Verify execution process exists and belongs to the task attempt
|
||||
let process = match ExecutionProcess::find_by_id(&app_state.db_pool, process_id).await {
|
||||
match ExecutionProcess::find_by_id(&app_state.db_pool, process_id).await {
|
||||
Ok(Some(process)) if process.task_attempt_id == attempt_id => process,
|
||||
Ok(Some(_)) => return Err(StatusCode::NOT_FOUND), // Process exists but wrong attempt
|
||||
Ok(None) => return Err(StatusCode::NOT_FOUND),
|
||||
@@ -864,33 +743,7 @@ pub async fn stop_execution_process(
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Create activity record for stopped processes (skip dev servers)
|
||||
if !matches!(
|
||||
process.process_type,
|
||||
crate::models::execution_process::ExecutionProcessType::DevServer
|
||||
) {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process_id,
|
||||
status: Some(TaskAttemptStatus::ExecutorFailed),
|
||||
note: Some(format!(
|
||||
"Execution process {:?} ({}) stopped by user",
|
||||
process.process_type, process_id
|
||||
)),
|
||||
};
|
||||
|
||||
if let Err(e) = TaskAttemptActivity::create(
|
||||
&app_state.db_pool,
|
||||
&create_activity,
|
||||
activity_id,
|
||||
TaskAttemptStatus::ExecutorFailed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Failed to create stopped activity: {}", e);
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
// Process stopped successfully
|
||||
|
||||
Ok(ResponseJson(ApiResponse {
|
||||
success: true,
|
||||
@@ -1556,10 +1409,7 @@ pub fn task_attempts_router() -> Router<AppState> {
|
||||
"/projects/:project_id/tasks/:task_id/attempts",
|
||||
get(get_task_attempts).post(create_task_attempt),
|
||||
)
|
||||
.route(
|
||||
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/activities",
|
||||
get(get_task_attempt_activities).post(create_task_attempt_activity),
|
||||
)
|
||||
|
||||
|
||||
.route(
|
||||
"/projects/:project_id/tasks/:task_id/attempts/:attempt_id/diff",
|
||||
|
||||
@@ -9,8 +9,7 @@ use crate::{
|
||||
executor_session::{CreateExecutorSession, ExecutorSession},
|
||||
project::Project,
|
||||
task::Task,
|
||||
task_attempt::{TaskAttempt, TaskAttemptError, TaskAttemptStatus},
|
||||
task_attempt_activity::{CreateTaskAttemptActivity, TaskAttemptActivity},
|
||||
task_attempt::{TaskAttempt, TaskAttemptError},
|
||||
},
|
||||
utils::shell::get_shell_command,
|
||||
};
|
||||
@@ -120,14 +119,7 @@ impl ProcessService {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create activity record
|
||||
Self::create_activity_record(
|
||||
pool,
|
||||
process_id,
|
||||
TaskAttemptStatus::SetupRunning,
|
||||
"Starting setup script with delegation",
|
||||
)
|
||||
.await?;
|
||||
// Setup script starting with delegation
|
||||
|
||||
tracing::info!(
|
||||
"Starting setup script with delegation to {} for task attempt {}",
|
||||
@@ -218,7 +210,6 @@ impl ProcessService {
|
||||
task_id,
|
||||
crate::executor::ExecutorType::CodingAgent(executor_config),
|
||||
"Starting executor".to_string(),
|
||||
TaskAttemptStatus::ExecutorRunning,
|
||||
ExecutionProcessType::CodingAgent,
|
||||
&task_attempt.worktree_path,
|
||||
)
|
||||
@@ -286,7 +277,6 @@ impl ProcessService {
|
||||
task_id,
|
||||
crate::executor::ExecutorType::DevServer(dev_script),
|
||||
"Starting dev server".to_string(),
|
||||
TaskAttemptStatus::ExecutorRunning, // Dev servers don't create activities, just use generic status
|
||||
ExecutionProcessType::DevServer,
|
||||
&worktree_path,
|
||||
)
|
||||
@@ -466,7 +456,6 @@ impl ProcessService {
|
||||
task_id,
|
||||
followup_executor,
|
||||
"Starting follow-up executor".to_string(),
|
||||
TaskAttemptStatus::ExecutorRunning,
|
||||
ExecutionProcessType::CodingAgent,
|
||||
&worktree_path,
|
||||
)
|
||||
@@ -492,7 +481,6 @@ impl ProcessService {
|
||||
task_id,
|
||||
new_session_executor,
|
||||
"Starting new executor session (follow-up session failed)".to_string(),
|
||||
TaskAttemptStatus::ExecutorRunning,
|
||||
ExecutionProcessType::CodingAgent,
|
||||
&worktree_path,
|
||||
)
|
||||
@@ -514,7 +502,6 @@ impl ProcessService {
|
||||
task_id: Uuid,
|
||||
executor_type: crate::executor::ExecutorType,
|
||||
activity_note: String,
|
||||
activity_status: TaskAttemptStatus,
|
||||
process_type: ExecutionProcessType,
|
||||
worktree_path: &str,
|
||||
) -> Result<(), TaskAttemptError> {
|
||||
@@ -550,11 +537,7 @@ impl ProcessService {
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Create activity record (skip for dev servers as they run in parallel)
|
||||
if !matches!(process_type, ExecutionProcessType::DevServer) {
|
||||
Self::create_activity_record(pool, process_id, activity_status.clone(), &activity_note)
|
||||
.await?;
|
||||
}
|
||||
// Process started successfully
|
||||
|
||||
tracing::info!("Starting {} for task attempt {}", activity_note, attempt_id);
|
||||
|
||||
@@ -625,7 +608,6 @@ impl ProcessService {
|
||||
task_id,
|
||||
crate::executor::ExecutorType::SetupScript(setup_script.clone()),
|
||||
"Starting setup script".to_string(),
|
||||
TaskAttemptStatus::SetupRunning,
|
||||
ExecutionProcessType::SetupScript,
|
||||
worktree_path,
|
||||
)
|
||||
@@ -721,26 +703,6 @@ impl ProcessService {
|
||||
.map_err(TaskAttemptError::from)
|
||||
}
|
||||
|
||||
/// Create activity record for process start
|
||||
async fn create_activity_record(
|
||||
pool: &SqlitePool,
|
||||
process_id: Uuid,
|
||||
activity_status: TaskAttemptStatus,
|
||||
activity_note: &str,
|
||||
) -> Result<(), TaskAttemptError> {
|
||||
let activity_id = Uuid::new_v4();
|
||||
let create_activity = CreateTaskAttemptActivity {
|
||||
execution_process_id: process_id,
|
||||
status: Some(activity_status.clone()),
|
||||
note: Some(activity_note.to_string()),
|
||||
};
|
||||
|
||||
TaskAttemptActivity::create(pool, &create_activity, activity_id, activity_status)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(TaskAttemptError::from)
|
||||
}
|
||||
|
||||
/// Execute the process based on type
|
||||
async fn execute_process(
|
||||
executor_type: &crate::executor::ExecutorType,
|
||||
|
||||
@@ -79,7 +79,6 @@ const TaskDetailsProvider: FC<{
|
||||
);
|
||||
|
||||
const [attemptData, setAttemptData] = useState<AttemptData>({
|
||||
activities: [],
|
||||
processes: [],
|
||||
runningProcessDetails: {},
|
||||
});
|
||||
@@ -234,29 +233,28 @@ const TaskDetailsProvider: FC<{
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const [activitiesResult, processesResult] = await Promise.all([
|
||||
attemptsApi.getActivities(projectId, taskId, attemptId),
|
||||
attemptsApi.getExecutionProcesses(projectId, taskId, attemptId),
|
||||
]);
|
||||
const processesResult = await attemptsApi.getExecutionProcesses(
|
||||
projectId,
|
||||
taskId,
|
||||
attemptId
|
||||
);
|
||||
|
||||
if (activitiesResult !== undefined && processesResult !== undefined) {
|
||||
const runningActivities = activitiesResult.filter(
|
||||
(activity) =>
|
||||
activity.status === 'setuprunning' ||
|
||||
activity.status === 'executorrunning'
|
||||
if (processesResult !== undefined) {
|
||||
const runningProcesses = processesResult.filter(
|
||||
(process) => process.status === 'running'
|
||||
);
|
||||
|
||||
const runningProcessDetails: Record<string, ExecutionProcess> = {};
|
||||
|
||||
// Fetch details for running activities
|
||||
for (const activity of runningActivities) {
|
||||
// Fetch details for running processes
|
||||
for (const process of runningProcesses) {
|
||||
const result = await executionProcessesApi.getDetails(
|
||||
projectId,
|
||||
activity.execution_process_id
|
||||
process.id
|
||||
);
|
||||
|
||||
if (result !== undefined) {
|
||||
runningProcessDetails[activity.execution_process_id] = result;
|
||||
runningProcessDetails[process.id] = result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +275,6 @@ const TaskDetailsProvider: FC<{
|
||||
|
||||
setAttemptData((prev: AttemptData) => {
|
||||
const newData = {
|
||||
activities: activitiesResult,
|
||||
processes: processesResult,
|
||||
runningProcessDetails,
|
||||
};
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronUp, Clock, Code } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Chip } from '@/components/ui/chip';
|
||||
import { NormalizedConversationViewer } from './TaskDetails/LogsTab/NormalizedConversationViewer.tsx';
|
||||
import type {
|
||||
ExecutionProcess,
|
||||
TaskAttempt,
|
||||
TaskAttemptActivityWithPrompt,
|
||||
TaskAttemptStatus,
|
||||
} from 'shared/types';
|
||||
|
||||
interface TaskActivityHistoryProps {
|
||||
selectedAttempt: TaskAttempt | null;
|
||||
activities: TaskAttemptActivityWithPrompt[];
|
||||
runningProcessDetails: Record<string, ExecutionProcess>;
|
||||
}
|
||||
|
||||
const getAttemptStatusDisplay = (
|
||||
status: TaskAttemptStatus
|
||||
): { label: string; dotColor: string } => {
|
||||
switch (status) {
|
||||
case 'setuprunning':
|
||||
return {
|
||||
label: 'Setup Running',
|
||||
dotColor: 'bg-blue-500',
|
||||
};
|
||||
case 'setupcomplete':
|
||||
return {
|
||||
label: 'Setup Complete',
|
||||
dotColor: 'bg-green-500',
|
||||
};
|
||||
case 'setupfailed':
|
||||
return {
|
||||
label: 'Setup Failed',
|
||||
dotColor: 'bg-red-500',
|
||||
};
|
||||
case 'executorrunning':
|
||||
return {
|
||||
label: 'Executor Running',
|
||||
dotColor: 'bg-blue-500',
|
||||
};
|
||||
case 'executorcomplete':
|
||||
return {
|
||||
label: 'Executor Complete',
|
||||
dotColor: 'bg-green-500',
|
||||
};
|
||||
case 'executorfailed':
|
||||
return {
|
||||
label: 'Executor Failed',
|
||||
dotColor: 'bg-red-500',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: 'Unknown',
|
||||
dotColor: 'bg-gray-400',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function TaskActivityHistory({
|
||||
selectedAttempt,
|
||||
activities,
|
||||
runningProcessDetails,
|
||||
}: TaskActivityHistoryProps) {
|
||||
const [expandedOutputs, setExpandedOutputs] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const toggleOutputExpansion = (processId: string) => {
|
||||
setExpandedOutputs((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(processId)) {
|
||||
newSet.delete(processId);
|
||||
} else {
|
||||
newSet.add(processId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
if (!selectedAttempt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label className="text-sm font-medium mb-3 block">Activity History</Label>
|
||||
{activities.length === 0 ? (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
No activities found
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{/* Fake worktree created activity */}
|
||||
<div key="worktree-created">
|
||||
<div className="flex items-center gap-3 my-4 rounded-md">
|
||||
<Chip dotColor="bg-green-500">New Worktree</Chip>
|
||||
<span className="text-sm text-muted-foreground flex-1">
|
||||
{selectedAttempt.worktree_path}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
{new Date(selectedAttempt.created_at).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activities.slice().map((activity) => (
|
||||
<div key={activity.id}>
|
||||
{/* Compact activity message */}
|
||||
<div className="flex items-center gap-3 my-4 rounded-md">
|
||||
<Chip
|
||||
dotColor={getAttemptStatusDisplay(activity.status).dotColor}
|
||||
>
|
||||
{getAttemptStatusDisplay(activity.status).label}
|
||||
</Chip>
|
||||
{activity.note && (
|
||||
<span className="text-sm text-muted-foreground flex-1">
|
||||
{activity.note}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
{new Date(activity.created_at).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show prompt for coding agent executions */}
|
||||
{activity.prompt && activity.status === 'executorrunning' && (
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="p-3 bg-blue-50 dark:bg-blue-950/30 rounded-md border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<Code className="h-4 w-4 text-blue-600 dark:text-blue-400 mt-0.5" />
|
||||
<span className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
Prompt
|
||||
</span>
|
||||
</div>
|
||||
<pre className="text-sm text-blue-800 dark:text-blue-200 whitespace-pre-wrap break-words">
|
||||
{activity.prompt}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show stdio output for running processes */}
|
||||
{(activity.status === 'setuprunning' ||
|
||||
activity.status === 'executorrunning') &&
|
||||
runningProcessDetails[activity.execution_process_id] && (
|
||||
<div className="mt-2">
|
||||
<div
|
||||
className={`transition-all duration-200 ${
|
||||
expandedOutputs.has(activity.execution_process_id)
|
||||
? ''
|
||||
: 'max-h-64 overflow-hidden flex flex-col justify-end'
|
||||
}`}
|
||||
>
|
||||
<NormalizedConversationViewer
|
||||
executionProcess={
|
||||
runningProcessDetails[activity.execution_process_id]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
toggleOutputExpansion(activity.execution_process_id)
|
||||
}
|
||||
className="mt-2 p-0 h-auto text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{expandedOutputs.has(activity.execution_process_id) ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3 mr-1" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3 mr-1" />
|
||||
Show more
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,12 +27,7 @@ function Conversation() {
|
||||
scrollContainerRef.current.scrollTop =
|
||||
scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [
|
||||
attemptData.activities,
|
||||
attemptData.processes,
|
||||
conversationUpdateTrigger,
|
||||
shouldAutoScrollLogs,
|
||||
]);
|
||||
}, [attemptData.processes, conversationUpdateTrigger, shouldAutoScrollLogs]);
|
||||
|
||||
const handleLogsScroll = useCallback(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
|
||||
@@ -171,7 +171,6 @@ function TaskDetailsToolbar() {
|
||||
} else {
|
||||
setSelectedAttempt(null);
|
||||
setAttemptData({
|
||||
activities: [],
|
||||
processes: [],
|
||||
runningProcessDetails: {},
|
||||
});
|
||||
|
||||
@@ -25,21 +25,22 @@ export function TaskFollowUpSection() {
|
||||
const canSendFollowUp = useMemo(() => {
|
||||
if (
|
||||
!selectedAttempt ||
|
||||
attemptData.activities.length === 0 ||
|
||||
attemptData.processes.length === 0 ||
|
||||
isAttemptRunning ||
|
||||
isSendingFollowUp
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const codingAgentActivities = attemptData.activities.filter(
|
||||
(activity) => activity.status === 'executorcomplete'
|
||||
const completedCodingAgentProcesses = attemptData.processes.filter(
|
||||
(process) =>
|
||||
process.process_type === 'codingagent' && process.status === 'completed'
|
||||
);
|
||||
|
||||
return codingAgentActivities.length > 0;
|
||||
return completedCodingAgentProcesses.length > 0;
|
||||
}, [
|
||||
selectedAttempt,
|
||||
attemptData.activities,
|
||||
attemptData.processes,
|
||||
isAttemptRunning,
|
||||
isSendingFollowUp,
|
||||
]);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ProjectWithBranch,
|
||||
Task,
|
||||
TaskAttempt,
|
||||
TaskAttemptActivityWithPrompt,
|
||||
TaskAttemptState,
|
||||
TaskTemplate,
|
||||
TaskWithAttemptStatus,
|
||||
@@ -318,17 +317,6 @@ export const attemptsApi = {
|
||||
return handleApiResponse<void>(response);
|
||||
},
|
||||
|
||||
getActivities: async (
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
attemptId: string
|
||||
): Promise<TaskAttemptActivityWithPrompt[]> => {
|
||||
const response = await makeRequest(
|
||||
`/api/projects/${projectId}/tasks/${taskId}/attempts/${attemptId}/activities`
|
||||
);
|
||||
return handleApiResponse<TaskAttemptActivityWithPrompt[]>(response);
|
||||
},
|
||||
|
||||
getDiff: async (
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
|
||||
@@ -2,11 +2,9 @@ import {
|
||||
DiffChunkType,
|
||||
ExecutionProcess,
|
||||
ExecutionProcessSummary,
|
||||
TaskAttemptActivityWithPrompt,
|
||||
} from 'shared/types.ts';
|
||||
|
||||
export type AttemptData = {
|
||||
activities: TaskAttemptActivityWithPrompt[];
|
||||
processes: ExecutionProcessSummary[];
|
||||
runningProcessDetails: Record<string, ExecutionProcess>;
|
||||
};
|
||||
|
||||
@@ -70,12 +70,6 @@ export type UpdateTaskAttempt = Record<string, never>;
|
||||
|
||||
export type CreateFollowUpAttempt = { prompt: string, };
|
||||
|
||||
export type TaskAttemptActivity = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, };
|
||||
|
||||
export type TaskAttemptActivityWithPrompt = { id: string, execution_process_id: string, status: TaskAttemptStatus, note: string | null, created_at: string, prompt: string | null, };
|
||||
|
||||
export type CreateTaskAttemptActivity = { execution_process_id: string, status: TaskAttemptStatus | null, note: string | null, };
|
||||
|
||||
export type DirectoryEntry = { name: string, path: string, is_directory: boolean, is_git_repo: boolean, };
|
||||
|
||||
export type DirectoryListResponse = { entries: Array<DirectoryEntry>, current_path: string, };
|
||||
|
||||
Reference in New Issue
Block a user