fix(runtime): cancel de background task immédiat et fiable depuis le workstate (#199, QA verte)

Transforme l'annulation d'une background task en contrat immédiat, observable et non-best-effort:

- domain/background_task: états Cancelling/Cancelled/CancelFailed et transitions idempotentes; un kill PTY qui échoue est surfacé en CancelFailed plutôt qu'en succès silencieux.

- infrastructure/runner + store: cancel effectif de la task ciblée, reconcile marque Cancelling même sans live handle, et départage CancelFailed des tâches pending au boot.

- application/background + workstate + orchestrator/wake: chemin de cancel initié depuis le workstate, projection synchronisée de l'état (pas d'attente d'un cycle ambigu).

- frontend: projection workstate de l'état cancelled/cancelling/cancelFailed (cancellingRow/cancelFailedRow/cancelButtons), normalization et services.

Tests QA verts: background_task_cancel, background_task_runner cancel_wins_even_if_output_is_still_available, background_task_store reconcile_marks_cancelling_without_live_handle_cancel_failed_and_pending, workstate.test.tsx.
This commit is contained in:
2026-08-12 15:18:38 +02:00
parent f5e2e302ba
commit 46103397de
24 changed files with 699 additions and 76 deletions

View File

@ -5161,9 +5161,11 @@ fn background_state_label(state: BackgroundTaskState) -> &'static str {
BackgroundTaskState::Queued => "queued",
BackgroundTaskState::Running => "running",
BackgroundTaskState::Waiting => "waiting",
BackgroundTaskState::Cancelling => "cancelling",
BackgroundTaskState::Completed => "completed",
BackgroundTaskState::Failed => "failed",
BackgroundTaskState::Cancelled => "cancelled",
BackgroundTaskState::CancelFailed => "cancelFailed",
BackgroundTaskState::Expired => "expired",
}
}
@ -5201,6 +5203,9 @@ impl From<BackgroundTask> for BackgroundTaskDto {
| Some(BackgroundTaskResult::Expired { reason, .. }) => {
(None, Some(reason.clone()), None, None)
}
Some(BackgroundTaskResult::CancelFailed { error, .. }) => {
(None, Some(error.clone()), None, None)
}
None => (None, None, None, None),
};
Self {

View File

@ -1357,6 +1357,7 @@ fn background_task_tail_bytes(task: &BackgroundTask) -> Vec<u8> {
.into_bytes(),
Some(BackgroundTaskResult::Cancelled { reason, .. })
| Some(BackgroundTaskResult::Expired { reason, .. }) => reason.clone().into_bytes(),
Some(BackgroundTaskResult::CancelFailed { error, .. }) => error.clone().into_bytes(),
None => Vec::new(),
}
}
@ -2452,10 +2453,14 @@ impl BackendCore {
)
.with_events(Arc::clone(&events_port)),
);
let cancel_background_task = Arc::new(CancelBackgroundTask::new(
Arc::clone(&background_tasks_port),
Arc::clone(&background_runner_port),
));
let cancel_background_task = Arc::new(
CancelBackgroundTask::new(
Arc::clone(&background_tasks_port),
Arc::clone(&background_runner_port),
Arc::clone(&clock) as Arc<dyn Clock>,
)
.with_events(Arc::clone(&events_port)),
);
let retry_background_task = Arc::new(RetryBackgroundTask::new(
Arc::clone(&background_tasks_port),
Arc::clone(&background_runner) as Arc<dyn BackgroundCommandArchive>,