Delete completed (and optionally failed) tasks that are not parents of active children. Removes terminal-state tasks from storage and deletes their working directories. Skips tasks that are referenced as parents by Queued or Running tasks to avoid orphaning child tasks that depend on them. # Arguments `include_failed` - When true, also clean Failed tasks (not just Complete) `min_age` - When Som
(
&self,
include_failed: bool,
min_age: Option<chrono::Duration>,
)
| 101 | /// * `min_age` - When Some, only clean tasks whose `completed_at` is older than this duration. |
| 102 | /// Tasks without a `completed_at` timestamp are never cleaned when an age filter is set. |
| 103 | pub async fn clean_tasks( |
| 104 | &self, |
| 105 | include_failed: bool, |
| 106 | min_age: Option<chrono::Duration>, |
| 107 | ) -> Result<CleanResult, String> { |
| 108 | // Get all tasks to find directories to delete |
| 109 | let all_tasks = self |
| 110 | .task_storage |
| 111 | .list_tasks() |
| 112 | .await |
| 113 | .map_err(|e| format!("Error listing tasks: {e}"))?; |
| 114 | |
| 115 | // Build set of task IDs that are parents of active (Queued/Running) children |
| 116 | let active_parent_ids: HashSet<String> = all_tasks |
| 117 | .iter() |
| 118 | .filter(|t| t.status == TaskStatus::Queued || t.status == TaskStatus::Running) |
| 119 | .flat_map(|t| t.parent_ids.iter().cloned()) |
| 120 | .collect(); |
| 121 | |
| 122 | let now = chrono::Utc::now(); |
| 123 | |
| 124 | // Filter for cleanable tasks based on status and age |
| 125 | let cleanable_tasks: Vec<&Task> = all_tasks |
| 126 | .iter() |
| 127 | .filter(|t| { |
| 128 | let status_match = t.status == TaskStatus::Complete |
| 129 | || (include_failed |
| 130 | && (t.status == TaskStatus::Failed || t.status == TaskStatus::Cancelled)); |
| 131 | if !status_match { |
| 132 | return false; |
| 133 | } |
| 134 | if let Some(min_age) = min_age { |
| 135 | match t.completed_at { |
| 136 | Some(completed_at) => now - completed_at >= min_age, |
| 137 | None => false, |
| 138 | } |
| 139 | } else { |
| 140 | true |
| 141 | } |
| 142 | }) |
| 143 | .collect(); |
| 144 | |
| 145 | let (deletable, skipped): (Vec<&Task>, Vec<&Task>) = cleanable_tasks |
| 146 | .into_iter() |
| 147 | .partition(|t| !active_parent_ids.contains(&t.id)); |
| 148 | |
| 149 | // Delete directories for deletable tasks |
| 150 | for task in &deletable { |
| 151 | // The task directory is the parent of the copied repo path |
| 152 | if let Some(ref copied_repo_path) = task.copied_repo_path |
| 153 | && let Some(task_dir) = copied_repo_path.parent() |
| 154 | && crate::file_system::exists(task_dir).await.unwrap_or(false) |
| 155 | && let Err(e) = crate::file_system::remove_dir(task_dir).await |
| 156 | { |
| 157 | eprintln!( |
| 158 | "Warning: Failed to delete task directory {}: {}", |
| 159 | task.id, e |
| 160 | ); |