1use std::collections::HashMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::sync::{Mutex, MutexGuard};
11
12use chrono::Utc;
13use pe_core::PeError;
14use serde::{Deserialize, Serialize};
15
16use crate::dependency::{self, TaskDependency};
17use crate::lifecycle::{DefaultLifecycle, TaskLifecycle};
18use crate::registry::{TaskFilter, TaskRegistry};
19use crate::task::{Task, TaskId, TaskStatus};
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22struct TaskStoreSnapshot {
23 #[serde(default = "current_version")]
24 version: u32,
25 #[serde(default)]
26 tasks: Vec<Task>,
27 #[serde(default)]
28 dependencies: Vec<TaskDependency>,
29}
30
31impl Default for TaskStoreSnapshot {
32 fn default() -> Self {
33 Self {
34 version: current_version(),
35 tasks: Vec::new(),
36 dependencies: Vec::new(),
37 }
38 }
39}
40
41fn current_version() -> u32 {
42 1
43}
44
45pub struct FileTaskRegistry {
51 path: PathBuf,
52 state: Mutex<TaskStoreSnapshot>,
53 lifecycle: Box<dyn TaskLifecycle>,
54}
55
56impl FileTaskRegistry {
57 pub fn new(path: impl Into<PathBuf>) -> Result<Self, PeError> {
59 Self::with_lifecycle(path, DefaultLifecycle)
60 }
61
62 pub fn with_lifecycle(
64 path: impl Into<PathBuf>,
65 lifecycle: impl TaskLifecycle + 'static,
66 ) -> Result<Self, PeError> {
67 let path = path.into();
68 let state = load_snapshot(&path)?;
69 Ok(Self {
70 path,
71 state: Mutex::new(state),
72 lifecycle: Box::new(lifecycle),
73 })
74 }
75
76 #[must_use]
78 pub fn path(&self) -> &Path {
79 &self.path
80 }
81
82 fn state_guard(&self) -> MutexGuard<'_, TaskStoreSnapshot> {
83 match self.state.lock() {
84 Ok(guard) => guard,
85 Err(poisoned) => poisoned.into_inner(),
86 }
87 }
88
89 fn persist_snapshot(&self, snapshot: &TaskStoreSnapshot) -> Result<(), PeError> {
90 if let Some(parent) = self.path.parent() {
91 fs::create_dir_all(parent).map_err(|error| PeError::Storage {
92 details: format!("failed to create task store {}: {error}", parent.display()),
93 })?;
94 }
95 let content = serde_json::to_string_pretty(snapshot).map_err(|error| PeError::Storage {
96 details: format!(
97 "failed to serialize task store {}: {error}",
98 self.path.display()
99 ),
100 })?;
101 fs::write(&self.path, content).map_err(|error| PeError::Storage {
102 details: format!(
103 "failed to write task store {}: {error}",
104 self.path.display()
105 ),
106 })
107 }
108
109 fn collect_tree(root_id: &str, tasks: &[Task]) -> Vec<Task> {
110 let mut result = Vec::new();
111 let mut stack = vec![root_id.to_string()];
112 while let Some(id) = stack.pop() {
113 for task in tasks {
114 if task.parent_task_id.as_deref() == Some(&id) && task.deleted_at.is_none() {
115 stack.push(task.id.clone());
116 result.push(task.clone());
117 }
118 }
119 }
120 result
121 }
122}
123
124#[async_trait::async_trait]
125impl TaskRegistry for FileTaskRegistry {
126 async fn create(&self, task: Task) -> Result<Task, PeError> {
127 {
128 let mut state = self.state_guard();
129 let mut next = state.clone();
130 dependency::validate_parent_link(&task, &next.tasks)?;
131 next.tasks.push(task.clone());
132 self.persist_snapshot(&next)?;
133 *state = next;
134 }
135 self.lifecycle.on_create(&task);
136 Ok(task)
137 }
138
139 async fn get(&self, id: &TaskId) -> Result<Option<Task>, PeError> {
140 let state = self.state_guard();
141 Ok(state
142 .tasks
143 .iter()
144 .find(|task| task.id == *id && task.deleted_at.is_none())
145 .cloned())
146 }
147
148 async fn update(&self, task: &Task) -> Result<Task, PeError> {
149 let mut updated = task.clone();
150 updated.updated_at = Some(Utc::now());
151
152 let mut state = self.state_guard();
153 let mut next = state.clone();
154 dependency::validate_parent_link(&updated, &next.tasks)?;
155 if let Some(existing) = next
156 .tasks
157 .iter_mut()
158 .find(|existing| existing.id == task.id)
159 {
160 *existing = updated.clone();
161 } else {
162 next.tasks.push(updated.clone());
163 }
164 self.persist_snapshot(&next)?;
165 *state = next;
166 Ok(updated)
167 }
168
169 async fn delete(&self, id: &TaskId) -> Result<bool, PeError> {
170 let mut state = self.state_guard();
171 let mut next = state.clone();
172 let Some(task) = next.tasks.iter_mut().find(|task| task.id == *id) else {
173 return Ok(false);
174 };
175 task.deleted_at = Some(Utc::now());
176 self.persist_snapshot(&next)?;
177 *state = next;
178 Ok(true)
179 }
180
181 async fn restore(&self, id: &TaskId) -> Result<bool, PeError> {
182 let mut state = self.state_guard();
183 let mut next = state.clone();
184 let Some(task) = next
185 .tasks
186 .iter_mut()
187 .find(|task| task.id == *id && task.deleted_at.is_some())
188 else {
189 return Ok(false);
190 };
191 task.deleted_at = None;
192 self.persist_snapshot(&next)?;
193 *state = next;
194 Ok(true)
195 }
196
197 async fn update_status(
198 &self,
199 id: &TaskId,
200 status: TaskStatus,
201 result: Option<serde_json::Value>,
202 error: Option<String>,
203 ) -> Result<Task, PeError> {
204 let (updated, old_status) = {
205 let mut state = self.state_guard();
206 let mut next = state.clone();
207 let task = next
208 .tasks
209 .iter_mut()
210 .find(|task| task.id == *id)
211 .ok_or_else(|| PeError::NodeNotFound { node: id.clone() })?;
212
213 self.lifecycle.validate_transition(task, &status)?;
214
215 let old_status = task.status.clone();
216 task.status = status.clone();
217 task.updated_at = Some(Utc::now());
218 if let Some(result) = result {
219 task.result = Some(result);
220 }
221 if let Some(error) = error {
222 task.error = Some(error);
223 }
224 if status == TaskStatus::Completed {
225 task.completed_at = Some(Utc::now());
226 }
227 let updated = task.clone();
228
229 self.persist_snapshot(&next)?;
230 *state = next;
231 (updated, old_status)
232 };
233
234 self.lifecycle.on_transition(&updated, &old_status);
235 match &status {
236 TaskStatus::Completed => self.lifecycle.on_complete(&updated),
237 TaskStatus::Failed => self
238 .lifecycle
239 .on_fail(&updated, updated.error.as_deref().unwrap_or("")),
240 TaskStatus::Cancelled => self.lifecycle.on_cancel(&updated),
241 _ => {}
242 }
243
244 Ok(updated)
245 }
246
247 async fn list(&self, filter: &TaskFilter) -> Result<Vec<Task>, PeError> {
248 let state = self.state_guard();
249 let limit = if filter.limit == 0 { 100 } else { filter.limit };
250 let results = state
251 .tasks
252 .iter()
253 .filter(|task| filter.include_deleted || task.deleted_at.is_none())
254 .filter(|task| filter.status.as_ref().is_none_or(|s| task.status == *s))
255 .filter(|task| {
256 filter
257 .task_type
258 .as_ref()
259 .is_none_or(|task_type| task.task_type == *task_type)
260 })
261 .filter(|task| {
262 filter
263 .priority
264 .as_ref()
265 .is_none_or(|priority| task.priority == *priority)
266 })
267 .filter(|task| {
268 filter
269 .agent_id
270 .as_ref()
271 .is_none_or(|agent| task.agent_id.as_ref() == Some(agent))
272 })
273 .filter(|task| {
274 filter
275 .assignee
276 .as_ref()
277 .is_none_or(|assignee| task.assignee == *assignee)
278 })
279 .filter(|task| {
280 filter
281 .parent_id
282 .as_ref()
283 .is_none_or(|parent| task.parent_task_id.as_ref() == Some(parent))
284 })
285 .filter(|task| {
286 filter
287 .tag
288 .as_ref()
289 .is_none_or(|tag| task.tags.contains(tag))
290 })
291 .take(limit)
292 .cloned()
293 .collect();
294 Ok(results)
295 }
296
297 async fn get_subtasks(&self, parent_id: &TaskId) -> Result<Vec<Task>, PeError> {
298 let state = self.state_guard();
299 Ok(state
300 .tasks
301 .iter()
302 .filter(|task| {
303 task.parent_task_id.as_ref() == Some(parent_id) && task.deleted_at.is_none()
304 })
305 .cloned()
306 .collect())
307 }
308
309 async fn get_tree(&self, root_id: &TaskId) -> Result<Vec<Task>, PeError> {
310 let state = self.state_guard();
311 Ok(Self::collect_tree(root_id, &state.tasks))
312 }
313
314 async fn add_dependency(&self, dep: TaskDependency) -> Result<(), PeError> {
315 let mut state = self.state_guard();
316 let mut next = state.clone();
317 dependency::validate_dependency_endpoints(&dep, &next.tasks)?;
318 dependency::validate_dependency_unique(&dep, &next.dependencies)?;
319 if dependency::would_create_cycle(&dep.task_id, &dep.depends_on_id, &next.dependencies) {
320 return Err(PeError::InvalidUpdate {
321 details: format!(
322 "Adding dependency {} -> {} would create a cycle",
323 dep.task_id, dep.depends_on_id
324 ),
325 });
326 }
327 next.dependencies.push(dep);
328 self.persist_snapshot(&next)?;
329 *state = next;
330 Ok(())
331 }
332
333 async fn remove_dependency(
334 &self,
335 task_id: &TaskId,
336 depends_on_id: &TaskId,
337 ) -> Result<(), PeError> {
338 let mut state = self.state_guard();
339 let mut next = state.clone();
340 next.dependencies
341 .retain(|dep| !(dep.task_id == *task_id && dep.depends_on_id == *depends_on_id));
342 self.persist_snapshot(&next)?;
343 *state = next;
344 Ok(())
345 }
346
347 async fn get_dependencies(&self, task_id: &TaskId) -> Result<Vec<TaskDependency>, PeError> {
348 let state = self.state_guard();
349 Ok(state
350 .dependencies
351 .iter()
352 .filter(|dep| dep.task_id == *task_id)
353 .cloned()
354 .collect())
355 }
356
357 async fn get_dependents(&self, task_id: &TaskId) -> Result<Vec<TaskDependency>, PeError> {
358 let state = self.state_guard();
359 Ok(state
360 .dependencies
361 .iter()
362 .filter(|dep| dep.depends_on_id == *task_id)
363 .cloned()
364 .collect())
365 }
366
367 async fn get_ready_tasks(&self) -> Result<Vec<Task>, PeError> {
368 let state = self.state_guard();
369 let pending = state
370 .tasks
371 .iter()
372 .filter(|task| task.status == TaskStatus::Pending && task.deleted_at.is_none())
373 .map(|task| task.id.clone())
374 .collect::<Vec<_>>();
375 let statuses: HashMap<TaskId, TaskStatus> = state
376 .tasks
377 .iter()
378 .map(|task| (task.id.clone(), task.status.clone()))
379 .collect();
380 let ready_ids = dependency::find_ready_tasks(&pending, &state.dependencies, &statuses);
381 Ok(ready_ids
382 .iter()
383 .filter_map(|id| state.tasks.iter().find(|task| task.id == *id).cloned())
384 .collect())
385 }
386}
387
388fn load_snapshot(path: &Path) -> Result<TaskStoreSnapshot, PeError> {
389 let content = match fs::read_to_string(path) {
390 Ok(content) => content,
391 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
392 return Ok(TaskStoreSnapshot::default());
393 }
394 Err(error) => {
395 return Err(PeError::Storage {
396 details: format!("failed to read task store {}: {error}", path.display()),
397 });
398 }
399 };
400 if content.trim().is_empty() {
401 return Ok(TaskStoreSnapshot::default());
402 }
403 serde_json::from_str(&content).map_err(|error| PeError::Storage {
404 details: format!("failed to parse task store {}: {error}", path.display()),
405 })
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::dependency::DependencyType;
412
413 fn temp_path(name: &str) -> PathBuf {
414 std::env::temp_dir().join(format!("pe-tasks-{name}-{}.json", uuid::Uuid::new_v4()))
415 }
416
417 #[tokio::test]
418 async fn persists_tasks_between_instances() {
419 let path = temp_path("persist");
420 let registry = FileTaskRegistry::new(&path).unwrap();
421 let created = registry
422 .create(Task::agent_task("Persistent task", "agent-1"))
423 .await
424 .unwrap();
425 drop(registry);
426
427 let reloaded = FileTaskRegistry::new(&path).unwrap();
428 let task = reloaded.get(&created.id).await.unwrap().unwrap();
429 assert_eq!(task.title, "Persistent task");
430 assert_eq!(task.agent_id.as_deref(), Some("agent-1"));
431
432 let _ = fs::remove_file(path);
433 }
434
435 #[tokio::test]
436 async fn persists_dependencies_between_instances() {
437 let path = temp_path("deps");
438 let registry = FileTaskRegistry::new(&path).unwrap();
439 let blocker = registry.create(Task::new("Blocker")).await.unwrap();
440 let blocked = registry.create(Task::new("Blocked")).await.unwrap();
441 registry
442 .add_dependency(TaskDependency::new(
443 &blocked.id,
444 &blocker.id,
445 DependencyType::Blocks,
446 ))
447 .await
448 .unwrap();
449 drop(registry);
450
451 let reloaded = FileTaskRegistry::new(&path).unwrap();
452 let deps = reloaded.get_dependencies(&blocked.id).await.unwrap();
453 assert_eq!(deps.len(), 1);
454 assert_eq!(deps[0].depends_on_id, blocker.id);
455
456 let _ = fs::remove_file(path);
457 }
458
459 #[tokio::test]
460 async fn validates_dependency_endpoints_and_duplicates() {
461 let path = temp_path("dep-validation");
462 let registry = FileTaskRegistry::new(&path).unwrap();
463 let blocker = registry.create(Task::new("Blocker")).await.unwrap();
464 let blocked = registry.create(Task::new("Blocked")).await.unwrap();
465
466 let err = registry
467 .add_dependency(TaskDependency::new(
468 &blocked.id,
469 "missing",
470 DependencyType::Blocks,
471 ))
472 .await
473 .unwrap_err();
474 assert!(matches!(err, PeError::NodeNotFound { node } if node == "missing"));
475
476 registry
477 .add_dependency(TaskDependency::new(
478 &blocked.id,
479 &blocker.id,
480 DependencyType::Blocks,
481 ))
482 .await
483 .unwrap();
484
485 let err = registry
486 .add_dependency(TaskDependency::new(
487 &blocked.id,
488 &blocker.id,
489 DependencyType::Related,
490 ))
491 .await
492 .unwrap_err();
493 assert!(
494 matches!(err, PeError::InvalidUpdate { details } if details.contains("already exists"))
495 );
496
497 let _ = fs::remove_file(path);
498 }
499
500 #[tokio::test]
501 async fn validates_parent_links() {
502 let path = temp_path("parent-validation");
503 let registry = FileTaskRegistry::new(&path).unwrap();
504
505 let err = registry
506 .create(Task::new("Orphan").with_parent("missing-parent"))
507 .await
508 .unwrap_err();
509 assert!(matches!(err, PeError::NodeNotFound { node } if node == "missing-parent"));
510
511 let parent = registry.create(Task::new("Parent")).await.unwrap();
512 let child = registry
513 .create(Task::new("Child").with_parent(&parent.id))
514 .await
515 .unwrap();
516 let mut updated_parent = parent.clone();
517 updated_parent.parent_task_id = Some(child.id.clone());
518
519 let err = registry.update(&updated_parent).await.unwrap_err();
520 assert!(matches!(err, PeError::InvalidUpdate { .. }));
521
522 let stored_parent = registry.get(&parent.id).await.unwrap().unwrap();
523 assert!(stored_parent.parent_task_id.is_none());
524
525 let _ = fs::remove_file(path);
526 }
527}