fix(runtime): cancel en cascade des tasks filles — stop agent / cancel parent (#200, QA verte)
Cancel transitif, idempotent et ownership-aware sur le graphe parent→enfants des background tasks: - domain/background_task + ports: modèle parent/enfant (parent_task_id, rejet du self-parent), index children_by_parent, traversal des descendants en largeur. - application/background + infrastructure/store: cascade cancel (annule les descendants running/open, laisse les terminaux), idempotente face aux double-cancel et ordres de terminaison non déterministes; respect de l'ownership (parent_task_link). - orchestrator/service: stop agent → cascade cancel des tasks lancées par cet agent (relatesTo #196); cancel task parent → cascade de ses sous-tasks (dependsOn #199). Refactor unifiant le chemin de cancel, sans sémantique #196/#199 regreffée (tests hérités verts). - frontend: projection parentTaskId (workstate normalization/domain/services). Tests QA verts: domain background_task, application background_task_cancel (cascades_background_task_subtree, cascades_to_running_child, cascades_to_open_descendants_and_leaves_terminal_child, parent_task_link_roundtrips_and_rejects_self_parent), orchestrator_service, workstate_actions, workstate.test.tsx.
This commit is contained in:
@ -130,6 +130,9 @@ pub struct BackgroundTask {
|
||||
/// Optional composite rendezvous this task participates in.
|
||||
#[serde(default)]
|
||||
pub rendezvous: Option<BackgroundTaskRendezvousLink>,
|
||||
/// Optional parent background task. Descendants are cancelled with their parent.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub parent_task_id: Option<TaskId>,
|
||||
/// Current lifecycle state.
|
||||
pub state: BackgroundTaskState,
|
||||
/// Completion wake policy.
|
||||
@ -167,6 +170,7 @@ impl BackgroundTask {
|
||||
owner_agent_id,
|
||||
kind,
|
||||
rendezvous: None,
|
||||
parent_task_id: None,
|
||||
state: BackgroundTaskState::Queued,
|
||||
wake_policy,
|
||||
created_at_ms: now_ms,
|
||||
@ -186,6 +190,16 @@ impl BackgroundTask {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a copy owned by a parent background task.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`BackgroundTaskError`] when the parent would make the task invalid.
|
||||
pub fn with_parent_task(mut self, parent_task_id: TaskId) -> Result<Self, BackgroundTaskError> {
|
||||
self.parent_task_id = Some(parent_task_id);
|
||||
self.validate()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Returns a copy moved to a non-terminal state.
|
||||
///
|
||||
/// Terminal transitions must use [`Self::complete`] so the result/state
|
||||
@ -293,6 +307,9 @@ impl BackgroundTask {
|
||||
if self.completion_delivered && !self.state.is_terminal() {
|
||||
return Err(BackgroundTaskError::MissingResult);
|
||||
}
|
||||
if self.parent_task_id == Some(self.id) {
|
||||
return Err(BackgroundTaskError::SelfParent);
|
||||
}
|
||||
if let Some(result) = &self.result {
|
||||
result.validate()?;
|
||||
if result.state() != self.state {
|
||||
@ -472,6 +489,9 @@ pub enum BackgroundTaskError {
|
||||
/// Completion delivery was already marked.
|
||||
#[error("background task completion was already delivered")]
|
||||
CompletionAlreadyDelivered,
|
||||
/// A task cannot own itself.
|
||||
#[error("background task cannot be its own parent")]
|
||||
SelfParent,
|
||||
}
|
||||
|
||||
fn is_transition_allowed(from: BackgroundTaskState, to: BackgroundTaskState) -> bool {
|
||||
|
||||
Reference in New Issue
Block a user