1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use awaken_runtime_contract::StateError;
6use parking_lot::RwLock;
7use tokio::sync::Mutex;
8use tokio::task::JoinHandle;
9
10mod run_cancellation;
11use crate::cancellation::{CancellationHandle, CancellationToken};
12use crate::inbox::InboxSender;
13use crate::state::{MutationBatch, StateStore};
14
15use super::state::{
16 BackgroundTaskStateAction, BackgroundTaskStateKey, BackgroundTaskStateSnapshot,
17 PersistedTaskMeta,
18};
19use super::types::{
20 AgentTaskContext, TaskContext, TaskEvent, TaskId, TaskParentContext, TaskResult, TaskStatus,
21 TaskSummary,
22};
23use super::{
24 BackgroundTaskExecutionContext, current_background_task_context, current_tool_lineage_context,
25 scope_background_task_context,
26};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum SendError {
31 TaskNotFound,
33 NotOwner,
35 TaskTerminated(TaskStatus),
37 NoInbox,
39 InboxClosed,
41}
42
43impl std::fmt::Display for SendError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::TaskNotFound => write!(f, "task not found"),
47 Self::NotOwner => write!(f, "caller does not own this task"),
48 Self::TaskTerminated(s) => write!(f, "task already {}", s.as_str()),
49 Self::NoInbox => write!(f, "task has no inbox (not a sub-agent)"),
50 Self::InboxClosed => write!(f, "sub-agent inbox closed"),
51 }
52 }
53}
54
55impl std::error::Error for SendError {}
56
57const RESERVED_NAMES: &[&str] = &["parent", "self", "all", "broadcast"];
59
60#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
62#[non_exhaustive]
63pub enum SpawnError {
64 #[error("'{0}' is a reserved name")]
66 ReservedName(String),
67 #[error("a running task named '{0}' already exists")]
69 DuplicateName(String),
70 #[error("background task state store is not configured")]
72 StoreNotConfigured,
73 #[error(transparent)]
75 State(#[from] StateError),
76 #[error("parent run '{0}' has already been cancelled")]
78 ParentRunCancelled(String),
79}
80
81#[derive(Debug, thiserror::Error)]
82enum MetaCommitError {
83 #[error("background task state store is not configured")]
84 StoreUnavailable,
85 #[error(transparent)]
86 State(#[from] StateError),
87}
88
89impl From<MetaCommitError> for SpawnError {
90 fn from(err: MetaCommitError) -> Self {
91 match err {
92 MetaCommitError::StoreUnavailable => Self::StoreNotConfigured,
93 MetaCommitError::State(e) => Self::State(e),
94 }
95 }
96}
97
98struct TaskHandle {
104 task_id: TaskId,
105 owner_thread_id: String,
106 cancel_handle: CancellationHandle,
107 _join_handle: JoinHandle<()>,
108 agent_inbox: Option<InboxSender>,
110}
111
112pub struct BackgroundTaskManager {
119 handles: Mutex<HashMap<TaskId, TaskHandle>>,
120 counter: AtomicU64,
121 owner_inbox: RwLock<Option<InboxSender>>,
122 store: std::sync::OnceLock<StateStore>,
123 cancelled_run_ids: RwLock<HashSet<String>>,
124}
125
126impl BackgroundTaskManager {
127 pub fn new() -> Self {
128 Self {
129 handles: Mutex::new(HashMap::new()),
130 counter: AtomicU64::new(0),
131 owner_inbox: RwLock::new(None),
132 store: std::sync::OnceLock::new(),
133 cancelled_run_ids: RwLock::new(HashSet::new()),
134 }
135 }
136
137 pub fn set_owner_inbox(&self, inbox: InboxSender) {
139 *self.owner_inbox.write() = Some(inbox);
140 }
141
142 pub fn set_store(&self, store: StateStore) {
147 let _ = self.store.set(store);
148 }
149
150 fn validate_name(&self, name: &str, owner_thread_id: &str) -> Result<(), SpawnError> {
152 if RESERVED_NAMES.contains(&name) {
153 return Err(SpawnError::ReservedName(name.to_string()));
154 }
155 if let Some(store) = self.store()
157 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
158 {
159 for meta in snap.tasks.values() {
160 if meta.owner_thread_id == owner_thread_id
161 && !meta.status.is_terminal()
162 && meta.name.as_deref() == Some(name)
163 {
164 return Err(SpawnError::DuplicateName(name.to_string()));
165 }
166 }
167 }
168 Ok(())
169 }
170
171 fn store(&self) -> Option<&StateStore> {
173 self.store.get()
174 }
175
176 fn owner_inbox(&self) -> Option<InboxSender> {
177 self.owner_inbox.read().clone()
178 }
179
180 #[cfg(test)]
181 pub(crate) fn panic_while_holding_owner_inbox_lock_for_test(&self) {
182 let _guard = self.owner_inbox.write();
183 panic!("owner_inbox lock test panic");
184 }
185
186 #[cfg(test)]
187 pub(crate) fn has_owner_inbox_for_test(&self) -> bool {
188 self.owner_inbox().is_some()
189 }
190
191 fn next_task_id(&self) -> TaskId {
192 let n = self.counter.fetch_add(1, Ordering::Relaxed);
193 format!("bg_{n}")
194 }
195
196 pub(crate) fn reserve_task_id(&self) -> TaskId {
201 self.next_task_id()
202 }
203
204 fn merge_ambient_parent_context(
205 &self,
206 mut parent_context: TaskParentContext,
207 ) -> TaskParentContext {
208 if parent_context.task_id.is_none()
209 && let Some(context) = current_background_task_context()
210 {
211 if parent_context.run_id.is_none() {
212 parent_context.run_id = context.run_id;
213 }
214 parent_context.task_id = Some(context.task_id);
215 }
216
217 if let Some(context) = current_tool_lineage_context() {
218 if parent_context.run_id.is_none() {
219 parent_context.run_id = Some(context.run_id);
220 }
221 if parent_context.call_id.is_none() && !context.call_id.is_empty() {
222 parent_context.call_id = Some(context.call_id);
223 }
224 if parent_context.agent_id.is_none() && !context.agent_id.is_empty() {
225 parent_context.agent_id = Some(context.agent_id);
226 }
227 }
228
229 parent_context
230 }
231
232 fn commit_meta(&self, action: BackgroundTaskStateAction) -> Result<u64, MetaCommitError> {
234 let Some(store) = self.store() else {
235 return Err(MetaCommitError::StoreUnavailable);
236 };
237
238 let mut batch = MutationBatch::new();
239 batch.update::<BackgroundTaskStateKey>(action);
240 store.commit(batch).map_err(Into::into)
241 }
242
243 fn commit_meta_or_warn(
244 &self,
245 action: BackgroundTaskStateAction,
246 operation: &'static str,
247 task_id: &str,
248 ) {
249 if let Err(error) = self.commit_meta(action) {
250 metrics::counter!(
251 "awaken_background_task_state_commit_failures_total",
252 "operation" => operation
253 )
254 .increment(1);
255 tracing::warn!(
256 operation,
257 task_id,
258 error = %error,
259 "background task metadata commit failed"
260 );
261 }
262 }
263
264 fn terminal_event(task_id: &str, result: &TaskResult) -> TaskEvent {
265 match result {
266 TaskResult::Success(val) => TaskEvent::Completed {
267 task_id: task_id.to_string(),
268 result: Some(val.clone()),
269 },
270 TaskResult::Failed(err) => TaskEvent::Failed {
271 task_id: task_id.to_string(),
272 error: err.clone(),
273 },
274 TaskResult::Cancelled => TaskEvent::Cancelled {
275 task_id: task_id.to_string(),
276 },
277 }
278 }
279
280 pub async fn spawn<F, Fut>(
286 self: &Arc<Self>,
287 owner_thread_id: &str,
288 task_type: &str,
289 name: Option<&str>,
290 description: &str,
291 parent_context: TaskParentContext,
292 task_fn: F,
293 ) -> Result<TaskId, SpawnError>
294 where
295 F: FnOnce(TaskContext) -> Fut + Send + 'static,
296 Fut: std::future::Future<Output = TaskResult> + Send + 'static,
297 {
298 let task_id = self.reserve_task_id();
299 self.spawn_with_task_id(
300 task_id,
301 owner_thread_id,
302 task_type,
303 name,
304 description,
305 parent_context,
306 task_fn,
307 )
308 .await
309 }
310
311 #[allow(clippy::too_many_arguments)]
312 pub(crate) async fn spawn_with_task_id<F, Fut>(
313 self: &Arc<Self>,
314 task_id: TaskId,
315 owner_thread_id: &str,
316 task_type: &str,
317 name: Option<&str>,
318 description: &str,
319 parent_context: TaskParentContext,
320 task_fn: F,
321 ) -> Result<TaskId, SpawnError>
322 where
323 F: FnOnce(TaskContext) -> Fut + Send + 'static,
324 Fut: std::future::Future<Output = TaskResult> + Send + 'static,
325 {
326 let parent_context = self.merge_ambient_parent_context(parent_context);
327 if let Some(n) = name {
328 self.validate_name(n, owner_thread_id)?;
329 }
330 if let Some(run_id) = self.is_parent_run_cancelled(&parent_context) {
331 return Err(SpawnError::ParentRunCancelled(run_id));
332 }
333 let (cancel_handle, cancel_token) = CancellationToken::new_pair();
334 let now = now_ms();
335
336 let ctx = TaskContext {
337 task_id: task_id.clone(),
338 cancel_token,
339 inbox: self.owner_inbox(),
340 };
341
342 let task_name = name.map(|n| n.to_string());
343
344 self.commit_meta(BackgroundTaskStateAction::Upsert(Box::new(
346 PersistedTaskMeta {
347 task_id: task_id.clone(),
348 owner_thread_id: owner_thread_id.to_string(),
349 task_type: task_type.to_string(),
350 name: task_name.clone(),
351 description: description.to_string(),
352 status: TaskStatus::Running,
353 error: None,
354 result: None,
355 created_at_ms: now,
356 completed_at_ms: None,
357 parent_context: parent_context.clone(),
358 },
359 )))
360 .map_err(SpawnError::from)?;
361
362 let manager = Arc::clone(self);
363 let tid = task_id.clone();
364 let owner_inbox = self.owner_inbox();
365 let owner = owner_thread_id.to_string();
366 let ttype = task_type.to_string();
367 let tname = task_name.clone();
368 let desc = description.to_string();
369 let parent_context_for_task = parent_context.clone();
370
371 let join_handle = tokio::spawn(async move {
372 let result = scope_background_task_context(
373 BackgroundTaskExecutionContext {
374 manager: manager.clone(),
375 task_id: tid.clone(),
376 run_id: parent_context_for_task.run_id.clone(),
377 },
378 task_fn(ctx),
379 )
380 .await;
381 let completed_at = now_ms();
382
383 let (status, error, result_val) = match &result {
385 TaskResult::Success(val) => (TaskStatus::Completed, None, Some(val.clone())),
386 TaskResult::Failed(err) => (TaskStatus::Failed, Some(err.clone()), None),
387 TaskResult::Cancelled => (TaskStatus::Cancelled, None, None),
388 };
389
390 manager.commit_meta_or_warn(
391 BackgroundTaskStateAction::Upsert(Box::new(PersistedTaskMeta {
392 task_id: tid.clone(),
393 owner_thread_id: owner,
394 task_type: ttype,
395 name: tname,
396 description: desc,
397 status,
398 error,
399 result: result_val,
400 created_at_ms: now,
401 completed_at_ms: Some(completed_at),
402 parent_context: parent_context_for_task,
403 })),
404 "task_completion",
405 &tid,
406 );
407
408 if let Some(ref inbox) = owner_inbox {
410 let event = Self::terminal_event(&tid, &result);
411 inbox.send(
412 serde_json::to_value(&event).expect("TaskEvent serialization is infallible"),
413 );
414 }
415 });
416
417 let handle = TaskHandle {
418 task_id: task_id.clone(),
419 owner_thread_id: owner_thread_id.to_string(),
420 cancel_handle,
421 _join_handle: join_handle,
422 agent_inbox: None,
423 };
424
425 self.insert_handle_and_cancel_if_parent_run_cancelled(
426 task_id.clone(),
427 handle,
428 &parent_context,
429 )
430 .await;
431 Ok(task_id)
432 }
433
434 pub async fn cancel(&self, task_id: &str) -> bool {
436 let handles = self.handles.lock().await;
437 if let Some(handle) = handles.get(task_id) {
438 if let Some(store) = self.store()
440 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
441 && let Some(meta) = snap.tasks.get(task_id)
442 && meta.status.is_terminal()
443 {
444 return false;
445 }
446 handle.cancel_handle.cancel();
447 return true;
448 }
449 false
450 }
451
452 pub async fn cancel_tree(&self, task_id: &str) -> usize {
457 let Some(task_ids) = self.task_tree_ids(task_id) else {
458 return 0;
459 };
460
461 let handles = self.handles.lock().await;
462 let store_snap = self
463 .store()
464 .and_then(|s| s.read::<BackgroundTaskStateKey>());
465 let mut count = 0;
466 for task_id in task_ids {
467 let Some(handle) = handles.get(&task_id) else {
468 continue;
469 };
470 let is_terminal = store_snap
471 .as_ref()
472 .and_then(|snap| snap.tasks.get(&task_id))
473 .map(|meta| meta.status.is_terminal())
474 .unwrap_or(false);
475 if !is_terminal {
476 handle.cancel_handle.cancel();
477 count += 1;
478 }
479 }
480 count
481 }
482
483 pub async fn cancel_all(&self, owner_thread_id: &str) -> usize {
486 let handles = self.handles.lock().await;
487 let store_snap = self
488 .store()
489 .and_then(|s| s.read::<BackgroundTaskStateKey>());
490 let mut count = 0;
491 for handle in handles.values() {
492 if handle.owner_thread_id != owner_thread_id {
493 continue;
494 }
495 let is_terminal = store_snap
496 .as_ref()
497 .and_then(|snap| snap.tasks.get(&handle.task_id))
498 .map(|m| m.status.is_terminal())
499 .unwrap_or(false);
500 if !is_terminal {
501 handle.cancel_handle.cancel();
502 count += 1;
503 }
504 }
505 count
506 }
507
508 pub async fn list(&self, owner_thread_id: &str) -> Vec<TaskSummary> {
510 if let Some(store) = self.store()
511 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
512 {
513 return snap
514 .tasks
515 .values()
516 .filter(|m| m.owner_thread_id == owner_thread_id)
517 .map(Self::meta_to_summary)
518 .collect();
519 }
520 Vec::new()
521 }
522
523 pub async fn get(&self, task_id: &str) -> Option<TaskSummary> {
525 self.store()
526 .and_then(|s| s.read::<BackgroundTaskStateKey>())
527 .and_then(|snap| snap.tasks.get(task_id).map(Self::meta_to_summary))
528 }
529
530 fn meta_to_summary(m: &PersistedTaskMeta) -> TaskSummary {
531 TaskSummary {
532 task_id: m.task_id.clone(),
533 task_type: m.task_type.clone(),
534 description: m.description.clone(),
535 status: m.status,
536 error: m.error.clone(),
537 result: m.result.clone(),
538 created_at_ms: m.created_at_ms,
539 completed_at_ms: m.completed_at_ms,
540 parent_context: m.parent_context.clone(),
541 }
542 }
543
544 pub(crate) async fn restore_for_thread(
546 &self,
547 owner_thread_id: &str,
548 snapshot: &BackgroundTaskStateSnapshot,
549 ) {
550 if let Some(store) = self.store() {
552 let existing = store.read::<BackgroundTaskStateKey>().unwrap_or_default();
554
555 for (task_id, meta) in &snapshot.tasks {
556 if existing.tasks.contains_key(task_id) {
557 continue;
558 }
559
560 if let Some(n) = task_id
562 .strip_prefix("bg_")
563 .and_then(|s| s.parse::<u64>().ok())
564 {
565 self.counter
566 .fetch_max(n.saturating_add(1), Ordering::Relaxed);
567 }
568
569 let handles = self.handles.lock().await;
570 let has_live_handle = handles.contains_key(task_id);
571 drop(handles);
572
573 let mut to_store = meta.clone();
574 to_store.owner_thread_id = owner_thread_id.to_string();
575
576 if meta.status == TaskStatus::Running && !has_live_handle {
578 to_store.status = TaskStatus::Failed;
579 to_store.error =
580 Some("task orphaned: runtime restarted while running".to_string());
581 }
582
583 self.commit_meta_or_warn(
584 BackgroundTaskStateAction::Upsert(Box::new(to_store)),
585 "restore_task_metadata",
586 task_id,
587 );
588 }
589 }
590 }
591
592 pub async fn has_running(&self, owner_thread_id: &str) -> bool {
594 if let Some(store) = self.store()
595 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
596 {
597 return snap
598 .tasks
599 .values()
600 .any(|m| m.owner_thread_id == owner_thread_id && !m.status.is_terminal());
601 }
602 self.handles
604 .lock()
605 .await
606 .values()
607 .any(|h| h.owner_thread_id == owner_thread_id)
608 }
609
610 pub async fn spawn_agent<F, Fut>(
614 self: &Arc<Self>,
615 owner_thread_id: &str,
616 name: Option<&str>,
617 description: &str,
618 parent_context: TaskParentContext,
619 task_fn: F,
620 ) -> Result<TaskId, SpawnError>
621 where
622 F: FnOnce(CancellationToken, InboxSender, crate::inbox::InboxReceiver) -> Fut
623 + Send
624 + 'static,
625 Fut: std::future::Future<Output = TaskResult> + Send + 'static,
626 {
627 self.spawn_agent_with_context(owner_thread_id, name, description, parent_context, |ctx| {
628 task_fn(ctx.cancel_token, ctx.inbox_sender, ctx.inbox_receiver)
629 })
630 .await
631 }
632
633 pub async fn spawn_agent_with_context<F, Fut>(
636 self: &Arc<Self>,
637 owner_thread_id: &str,
638 name: Option<&str>,
639 description: &str,
640 parent_context: TaskParentContext,
641 task_fn: F,
642 ) -> Result<TaskId, SpawnError>
643 where
644 F: FnOnce(AgentTaskContext) -> Fut + Send + 'static,
645 Fut: std::future::Future<Output = TaskResult> + Send + 'static,
646 {
647 let parent_context = self.merge_ambient_parent_context(parent_context);
648 if let Some(n) = name {
649 self.validate_name(n, owner_thread_id)?;
650 }
651 if let Some(run_id) = self.is_parent_run_cancelled(&parent_context) {
652 return Err(SpawnError::ParentRunCancelled(run_id));
653 }
654 let task_id = self.next_task_id();
655 let (cancel_handle, cancel_token) = CancellationToken::new_pair();
656 let now = now_ms();
657
658 let (child_inbox_tx, child_inbox_rx) = crate::inbox::inbox_channel();
659 let stored_sender = child_inbox_tx.clone();
660
661 let task_name = name.map(|n| n.to_string());
662
663 self.commit_meta(BackgroundTaskStateAction::Upsert(Box::new(
665 PersistedTaskMeta {
666 task_id: task_id.clone(),
667 owner_thread_id: owner_thread_id.to_string(),
668 task_type: "sub_agent".to_string(),
669 name: task_name.clone(),
670 description: description.to_string(),
671 status: TaskStatus::Running,
672 error: None,
673 result: None,
674 created_at_ms: now,
675 completed_at_ms: None,
676 parent_context: parent_context.clone(),
677 },
678 )))
679 .map_err(SpawnError::from)?;
680
681 let manager = Arc::clone(self);
682 let tid = task_id.clone();
683 let owner_inbox = self.owner_inbox();
684 let owner = owner_thread_id.to_string();
685 let tname = task_name.clone();
686 let desc = description.to_string();
687 let parent_context_for_task = parent_context.clone();
688
689 let join_handle = tokio::spawn(async move {
690 let result = scope_background_task_context(
691 BackgroundTaskExecutionContext {
692 manager: manager.clone(),
693 task_id: tid.clone(),
694 run_id: parent_context_for_task.run_id.clone(),
695 },
696 task_fn(AgentTaskContext {
697 task_id: tid.clone(),
698 cancel_token,
699 inbox_sender: child_inbox_tx,
700 inbox_receiver: child_inbox_rx,
701 }),
702 )
703 .await;
704 let completed_at = now_ms();
705
706 let (status, error, result_val) = match &result {
707 TaskResult::Success(val) => (TaskStatus::Completed, None, Some(val.clone())),
708 TaskResult::Failed(err) => (TaskStatus::Failed, Some(err.clone()), None),
709 TaskResult::Cancelled => (TaskStatus::Cancelled, None, None),
710 };
711
712 manager.commit_meta_or_warn(
713 BackgroundTaskStateAction::Upsert(Box::new(PersistedTaskMeta {
714 task_id: tid.clone(),
715 owner_thread_id: owner,
716 task_type: "sub_agent".to_string(),
717 name: tname,
718 description: desc,
719 status,
720 error,
721 result: result_val,
722 created_at_ms: now,
723 completed_at_ms: Some(completed_at),
724 parent_context: parent_context_for_task,
725 })),
726 "sub_agent_completion",
727 &tid,
728 );
729
730 let event = Self::terminal_event(&tid, &result);
731 if let Some(ref inbox) = owner_inbox {
732 inbox.send(
733 serde_json::to_value(&event).expect("TaskEvent serialization is infallible"),
734 );
735 }
736 });
737
738 let handle = TaskHandle {
739 task_id: task_id.clone(),
740 owner_thread_id: owner_thread_id.to_string(),
741 cancel_handle,
742 _join_handle: join_handle,
743 agent_inbox: Some(stored_sender),
744 };
745
746 self.insert_handle_and_cancel_if_parent_run_cancelled(
747 task_id.clone(),
748 handle,
749 &parent_context,
750 )
751 .await;
752 Ok(task_id)
753 }
754
755 pub async fn send_task_inbox_message(
761 &self,
762 task_id: &str,
763 owner_thread_id: &str,
764 sender_agent_id: &str,
765 content: &str,
766 ) -> Result<(), SendError> {
767 let handles = self.handles.lock().await;
768 let handle = handles.get(task_id).ok_or(SendError::TaskNotFound)?;
769
770 if handle.owner_thread_id != owner_thread_id {
772 return Err(SendError::NotOwner);
773 }
774
775 if let Some(store) = self.store()
777 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
778 && let Some(meta) = snap.tasks.get(task_id)
779 && meta.status.is_terminal()
780 {
781 return Err(SendError::TaskTerminated(meta.status));
782 }
783
784 let inbox = handle.agent_inbox.as_ref().ok_or(SendError::NoInbox)?;
785
786 let event = TaskEvent::Custom {
787 task_id: task_id.to_string(),
788 event_type: "agent_message".to_string(),
789 payload: serde_json::json!({
790 "from": sender_agent_id,
791 "content": content,
792 }),
793 };
794
795 if inbox.send(serde_json::to_value(&event).expect("TaskEvent serialization is infallible"))
796 {
797 Ok(())
798 } else {
799 Err(SendError::InboxClosed)
800 }
801 }
802
803 pub(crate) fn task_tree_ids(&self, task_id: &str) -> Option<Vec<TaskId>> {
804 let snapshot = self
805 .store()
806 .and_then(|store| store.read::<BackgroundTaskStateKey>())?;
807 if !snapshot.tasks.contains_key(task_id) {
808 return None;
809 }
810
811 let mut ordered = Vec::new();
812 let mut stack = vec![task_id.to_string()];
813 while let Some(current) = stack.pop() {
814 if ordered.iter().any(|seen| seen == ¤t) {
815 continue;
816 }
817 ordered.push(current.clone());
818 for meta in snapshot.tasks.values() {
819 if meta.parent_context.task_id.as_deref() == Some(current.as_str()) {
820 stack.push(meta.task_id.clone());
821 }
822 }
823 }
824 Some(ordered)
825 }
826
827 pub(crate) fn resolve_live_child_task(
828 &self,
829 parent_task_id: &str,
830 name_or_task_id: &str,
831 ) -> Option<TaskId> {
832 let snapshot = self.store()?.read::<BackgroundTaskStateKey>()?;
833 for meta in snapshot.tasks.values() {
834 if meta.status.is_terminal() {
835 continue;
836 }
837 if meta.parent_context.task_id.as_deref() != Some(parent_task_id) {
838 continue;
839 }
840 if meta.task_id == name_or_task_id || meta.name.as_deref() == Some(name_or_task_id) {
841 return Some(meta.task_id.clone());
842 }
843 }
844 None
845 }
846
847 pub(crate) fn resolve_live_child_run(
848 &self,
849 parent_run_id: &str,
850 name_or_task_id: &str,
851 ) -> Option<TaskId> {
852 let snapshot = self.store()?.read::<BackgroundTaskStateKey>()?;
853 for meta in snapshot.tasks.values() {
854 if meta.status.is_terminal() {
855 continue;
856 }
857 if meta.parent_context.run_id.as_deref() != Some(parent_run_id)
858 || meta.parent_context.task_id.is_some()
859 {
860 continue;
861 }
862 if meta.task_id == name_or_task_id || meta.name.as_deref() == Some(name_or_task_id) {
863 return Some(meta.task_id.clone());
864 }
865 }
866 None
867 }
868
869 #[cfg(test)]
870 pub(crate) async fn persisted_snapshot(&self) -> HashMap<TaskId, PersistedTaskMeta> {
871 if let Some(store) = self.store()
872 && let Some(snap) = store.read::<BackgroundTaskStateKey>()
873 {
874 return snap.tasks;
875 }
876 HashMap::new()
877 }
878}
879
880impl Default for BackgroundTaskManager {
881 fn default() -> Self {
882 Self::new()
883 }
884}
885
886use awaken_runtime_contract::now_ms;