ralph_workflow/pipeline/idle_timeout/
file_activity.rs1use crate::workspace::Workspace;
14use std::path::Path;
15use std::time::{Duration, SystemTime};
16
17pub struct FileActivityTracker {
18 _private: (),
19}
20
21impl FileActivityTracker {
22 #[must_use]
23 pub const fn new() -> Self {
24 Self { _private: () }
25 }
26
27 pub fn check_for_recent_activity(
28 &self,
29 workspace: &dyn Workspace,
30 timeout: Duration,
31 now: SystemTime,
32 ) -> std::io::Result<bool> {
33 check_for_recent_activity_with_time(workspace, timeout, now)
34 }
35
36 fn is_ai_generated_file(path: &Path) -> bool {
37 let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
38 return false;
39 };
40
41 let has_excluded_ext = path.extension().is_some_and(|ext| {
42 ext.eq_ignore_ascii_case("log")
43 || ext.eq_ignore_ascii_case("swp")
44 || ext.eq_ignore_ascii_case("tmp")
45 || ext.eq_ignore_ascii_case("bak")
46 });
47
48 if has_excluded_ext
49 || file_name == "checkpoint.json"
50 || file_name == "start_commit"
51 || file_name == "review_baseline.txt"
52 || file_name.ends_with('~')
53 {
54 return false;
55 }
56
57 matches!(
58 file_name,
59 "PLAN.md" | "ISSUES.md" | "NOTES.md" | "STATUS.md" | "commit-message.txt"
60 )
61 }
62}
63
64impl Default for FileActivityTracker {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70fn file_age(now: SystemTime, mtime: SystemTime) -> Duration {
71 now.duration_since(mtime).unwrap_or(Duration::ZERO)
72}
73
74fn check_for_recent_activity_with_time(
75 workspace: &dyn Workspace,
76 timeout: Duration,
77 now: SystemTime,
78) -> std::io::Result<bool> {
79 let agent_dir = Path::new(".agent");
80
81 if workspace.exists(agent_dir) {
82 let entries = workspace.read_dir(agent_dir)?;
83
84 let has_recent_activity = entries
85 .into_iter()
86 .filter(|entry| entry.is_file())
87 .filter_map(|entry| {
88 let path = entry.path();
89 if !FileActivityTracker::is_ai_generated_file(path) {
90 return None;
91 }
92 entry.modified().map(|mtime| (path.to_path_buf(), mtime))
93 })
94 .any(|(_, mtime)| file_age(now, mtime) <= timeout);
95
96 if has_recent_activity {
97 return Ok(true);
98 }
99 }
100
101 let tmp_dir = Path::new(".agent/tmp");
102 if workspace.exists(tmp_dir) {
103 if let Ok(tmp_entries) = workspace.read_dir(tmp_dir) {
104 let has_recent_xml = tmp_entries
105 .into_iter()
106 .filter(|entry| entry.is_file())
107 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "xml"))
108 .filter_map(|entry| entry.modified())
109 .any(|mtime| file_age(now, mtime) <= timeout);
110
111 if has_recent_xml {
112 return Ok(true);
113 }
114 }
115 }
116
117 Ok(false)
118}