1use rusqlite::params;
19
20use super::error::StoreError;
21use super::store::Store;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum BackgroundTaskStatus {
25 Running,
26 Done,
27 Error,
28 Stopped,
29}
30
31impl BackgroundTaskStatus {
32 pub fn as_str(self) -> &'static str {
33 match self {
34 BackgroundTaskStatus::Running => "running",
35 BackgroundTaskStatus::Done => "done",
36 BackgroundTaskStatus::Error => "error",
37 BackgroundTaskStatus::Stopped => "stopped",
38 }
39 }
40
41 pub fn from_str(s: &str) -> Self {
49 match s {
50 "done" => BackgroundTaskStatus::Done,
51 "error" => BackgroundTaskStatus::Error,
52 "stopped" => BackgroundTaskStatus::Stopped,
53 _ => BackgroundTaskStatus::Running,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct BackgroundTask {
60 pub id: String,
61 pub block_id: String,
62 pub label: String,
63 pub pid: Option<i64>,
64 pub started_at_ms: i64,
65 pub status: BackgroundTaskStatus,
66 pub last_seen_ms: i64,
67 pub ended_at_ms: Option<i64>,
68 pub owner_tool_use_id: Option<String>,
71}
72
73const COLUMNS: &str =
74 "id, block_id, label, pid, started_at_ms, status, last_seen_ms, ended_at_ms, owner_tool_use_id";
75
76impl Store {
77 pub fn background_task_observe(
82 &self,
83 id: &str,
84 block_id: &str,
85 label: &str,
86 started_at_ms: i64,
87 now_ms: i64,
88 ) -> Result<(), StoreError> {
89 let conn = self.conn.lock().unwrap();
90 conn.execute(
91 "INSERT OR IGNORE INTO db_background_tasks
92 (id, block_id, label, pid, started_at_ms, status, last_seen_ms, ended_at_ms)
93 VALUES (?1, ?2, ?3, NULL, ?4, 'running', ?5, NULL)",
94 params![id, block_id, label, started_at_ms, now_ms],
95 )?;
96 conn.execute(
97 "UPDATE db_background_tasks SET last_seen_ms = ?1 WHERE id = ?2 AND status = 'running'",
98 params![now_ms, id],
99 )?;
100 Ok(())
101 }
102
103 pub fn background_task_set_pid(&self, id: &str, pid: i64) -> Result<bool, StoreError> {
107 let conn = self.conn.lock().unwrap();
108 let rows = conn.execute(
109 "UPDATE db_background_tasks SET pid = ?1 WHERE id = ?2",
110 params![pid, id],
111 )?;
112 Ok(rows > 0)
113 }
114
115 pub fn background_task_complete(
121 &self,
122 id: &str,
123 status: BackgroundTaskStatus,
124 ended_at_ms: i64,
125 ) -> Result<bool, StoreError> {
126 let conn = self.conn.lock().unwrap();
127 let rows = conn.execute(
128 "UPDATE db_background_tasks
129 SET status = ?1, ended_at_ms = ?2, last_seen_ms = ?2
130 WHERE id = ?3",
131 params![status.as_str(), ended_at_ms, id],
132 )?;
133 Ok(rows > 0)
134 }
135
136 pub fn background_task_get(&self, id: &str) -> Result<Option<BackgroundTask>, StoreError> {
137 let conn = self.conn.lock().unwrap();
138 let mut stmt = conn.prepare(&format!("SELECT {COLUMNS} FROM db_background_tasks WHERE id = ?1"))?;
139 let mut rows = stmt.query_map(params![id], map_row)?;
140 match rows.next() {
141 Some(r) => Ok(Some(r?)),
142 None => Ok(None),
143 }
144 }
145
146 pub fn background_task_list_for_block(&self, block_id: &str) -> Result<Vec<BackgroundTask>, StoreError> {
147 let conn = self.conn.lock().unwrap();
148 let mut stmt = conn.prepare(&format!(
149 "SELECT {COLUMNS} FROM db_background_tasks WHERE block_id = ?1 ORDER BY started_at_ms ASC"
150 ))?;
151 let iter = stmt.query_map(params![block_id], map_row)?;
152 iter.collect::<Result<Vec<_>, _>>().map_err(Into::into)
153 }
154
155 pub fn background_task_list_running(&self) -> Result<Vec<BackgroundTask>, StoreError> {
159 let conn = self.conn.lock().unwrap();
160 let mut stmt = conn.prepare(&format!(
161 "SELECT {COLUMNS} FROM db_background_tasks WHERE status = 'running' ORDER BY started_at_ms ASC"
162 ))?;
163 let iter = stmt.query_map([], map_row)?;
164 iter.collect::<Result<Vec<_>, _>>().map_err(Into::into)
165 }
166
167 pub fn background_task_list_fleet(&self, ended_since_ms: i64) -> Result<Vec<BackgroundTask>, StoreError> {
171 let conn = self.conn.lock().unwrap();
172 let mut stmt = conn.prepare(&format!(
173 "SELECT {COLUMNS} FROM db_background_tasks
174 WHERE status = 'running' OR ended_at_ms >= ?1
175 ORDER BY started_at_ms ASC"
176 ))?;
177 let iter = stmt.query_map(params![ended_since_ms], map_row)?;
178 iter.collect::<Result<Vec<_>, _>>().map_err(Into::into)
179 }
180
181 pub fn background_task_describe(
186 &self,
187 id: &str,
188 label: &str,
189 owner_tool_use_id: Option<&str>,
190 ) -> Result<bool, StoreError> {
191 let conn = self.conn.lock().unwrap();
192 let rows = conn.execute(
193 "UPDATE db_background_tasks
194 SET label = ?1, owner_tool_use_id = COALESCE(owner_tool_use_id, ?2)
195 WHERE id = ?3",
196 params![label, owner_tool_use_id, id],
197 )?;
198 Ok(rows > 0)
199 }
200}
201
202fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<BackgroundTask> {
203 let status: String = row.get(5)?;
204 Ok(BackgroundTask {
205 id: row.get(0)?,
206 block_id: row.get(1)?,
207 label: row.get(2)?,
208 pid: row.get(3)?,
209 started_at_ms: row.get(4)?,
210 status: BackgroundTaskStatus::from_str(&status),
211 last_seen_ms: row.get(6)?,
212 ended_at_ms: row.get(7)?,
213 owner_tool_use_id: row.get(8)?,
214 })
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn object_store() -> Store {
222 Store::open(std::path::Path::new(":memory:")).unwrap()
223 }
224
225 #[test]
226 fn observe_creates_a_running_row() {
227 let store = object_store();
228 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
229 let task = store.background_task_get("t1").unwrap().unwrap();
230 assert_eq!(task.block_id, "block-a");
231 assert_eq!(task.label, "task dev");
232 assert_eq!(task.status, BackgroundTaskStatus::Running);
233 assert_eq!(task.started_at_ms, 1000);
234 assert_eq!(task.last_seen_ms, 1000);
235 assert_eq!(task.pid, None);
236 assert_eq!(task.ended_at_ms, None);
237 }
238
239 #[test]
240 fn observe_is_idempotent_and_refreshes_last_seen() {
241 let store = object_store();
242 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
243 store.background_task_observe("t1", "block-a", "task dev", 1000, 5000).unwrap();
244 let task = store.background_task_get("t1").unwrap().unwrap();
245 assert_eq!(task.started_at_ms, 1000);
248 assert_eq!(task.last_seen_ms, 5000);
249 }
250
251 #[test]
252 fn observe_never_resurrects_a_completed_task() {
253 let store = object_store();
254 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
255 store.background_task_complete("t1", BackgroundTaskStatus::Done, 2000).unwrap();
256 store.background_task_observe("t1", "block-a", "task dev", 1000, 9000).unwrap();
259 let task = store.background_task_get("t1").unwrap().unwrap();
260 assert_eq!(task.status, BackgroundTaskStatus::Done);
261 assert_eq!(task.last_seen_ms, 2000);
262 }
263
264 #[test]
265 fn complete_marks_terminal_and_returns_true_when_row_exists() {
266 let store = object_store();
267 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
268 let changed = store.background_task_complete("t1", BackgroundTaskStatus::Error, 4000).unwrap();
269 assert!(changed);
270 let task = store.background_task_get("t1").unwrap().unwrap();
271 assert_eq!(task.status, BackgroundTaskStatus::Error);
272 assert_eq!(task.ended_at_ms, Some(4000));
273 }
274
275 #[test]
276 fn complete_returns_false_for_unknown_id() {
277 let store = object_store();
278 assert!(!store.background_task_complete("nope", BackgroundTaskStatus::Done, 1).unwrap());
279 }
280
281 #[test]
282 fn set_pid_updates_existing_row_and_no_ops_for_unknown_id() {
283 let store = object_store();
284 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
285 assert!(store.background_task_set_pid("t1", 4242).unwrap());
286 assert_eq!(store.background_task_get("t1").unwrap().unwrap().pid, Some(4242));
287 assert!(!store.background_task_set_pid("nope", 1).unwrap());
288 }
289
290 #[test]
291 fn get_returns_none_for_unknown_id() {
292 let store = object_store();
293 assert!(store.background_task_get("nope").unwrap().is_none());
294 }
295
296 #[test]
297 fn list_for_block_only_returns_that_blocks_tasks_in_start_order() {
298 let store = object_store();
299 store.background_task_observe("t1", "block-a", "first", 1000, 1000).unwrap();
300 store.background_task_observe("t2", "block-b", "other-block", 1500, 1500).unwrap();
301 store.background_task_observe("t3", "block-a", "second", 2000, 2000).unwrap();
302 let tasks = store.background_task_list_for_block("block-a").unwrap();
303 let ids: Vec<&str> = tasks.iter().map(|t| t.id.as_str()).collect();
304 assert_eq!(ids, vec!["t1", "t3"]);
305 }
306
307 #[test]
308 fn list_running_excludes_terminal_tasks_across_all_blocks() {
309 let store = object_store();
310 store.background_task_observe("t1", "block-a", "still running", 1000, 1000).unwrap();
311 store.background_task_observe("t2", "block-b", "finished", 1000, 1000).unwrap();
312 store.background_task_complete("t2", BackgroundTaskStatus::Done, 2000).unwrap();
313 let running = store.background_task_list_running().unwrap();
314 let ids: Vec<&str> = running.iter().map(|t| t.id.as_str()).collect();
315 assert_eq!(ids, vec!["t1"]);
316 }
317
318 #[test]
319 fn status_round_trips_through_str_conversion() {
320 for s in [
321 BackgroundTaskStatus::Running,
322 BackgroundTaskStatus::Done,
323 BackgroundTaskStatus::Error,
324 BackgroundTaskStatus::Stopped,
325 ] {
326 assert_eq!(BackgroundTaskStatus::from_str(s.as_str()), s);
327 }
328 }
329
330 #[test]
331 fn unrecognized_status_string_defaults_to_running() {
332 assert_eq!(BackgroundTaskStatus::from_str("garbage"), BackgroundTaskStatus::Running);
333 }
334
335 #[test]
340 fn survives_reopening_the_store() {
341 let tmp = tempfile::NamedTempFile::new().unwrap();
342 {
343 let store = Store::open(tmp.path()).unwrap();
344 store.background_task_observe("t1", "block-a", "task dev", 1000, 1000).unwrap();
345 }
346 let reopened = Store::open(tmp.path()).unwrap();
347 let task = reopened.background_task_get("t1").unwrap().unwrap();
348 assert_eq!(task.label, "task dev");
349 assert_eq!(task.status, BackgroundTaskStatus::Running);
350 }
351
352 #[test]
353 fn describe_replaces_the_label_and_fills_the_owner_once() {
354 let store = object_store();
355 store.background_task_observe("t1", "block-a", "Bash", 1000, 1000).unwrap();
356 assert_eq!(store.background_task_get("t1").unwrap().unwrap().owner_tool_use_id, None);
357
358 assert!(store.background_task_describe("t1", "Background wait", Some("toolu_agent")).unwrap());
359 let task = store.background_task_get("t1").unwrap().unwrap();
360 assert_eq!((task.label.as_str(), task.owner_tool_use_id.as_deref()), ("Background wait", Some("toolu_agent")));
361
362 store.background_task_describe("t1", "Background wait", None).unwrap();
365 store.background_task_describe("t1", "Background wait", Some("toolu_other")).unwrap();
366 assert_eq!(store.background_task_get("t1").unwrap().unwrap().owner_tool_use_id.as_deref(), Some("toolu_agent"));
367
368 assert!(!store.background_task_describe("missing", "x", None).unwrap());
369 }
370
371 #[test]
372 fn list_fleet_returns_running_rows_and_recently_ended_ones_across_blocks() {
373 let store = object_store();
374 store.background_task_observe("live", "block-a", "live", 100, 100).unwrap();
375 store.background_task_observe("recent", "block-b", "recent", 110, 110).unwrap();
376 store.background_task_observe("old", "block-b", "old", 50, 50).unwrap();
377 store.background_task_complete("recent", BackgroundTaskStatus::Done, 900).unwrap();
378 store.background_task_complete("old", BackgroundTaskStatus::Error, 200).unwrap();
379
380 let ids: Vec<String> = store.background_task_list_fleet(500).unwrap().into_iter().map(|t| t.id).collect();
381 assert_eq!(ids, vec!["live".to_string(), "recent".to_string()]);
382 }
383}