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:
@ -92,10 +92,14 @@ function formatTaskStatus(status: BackgroundCompletion["status"]): string {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return "Running";
|
||||
case "cancelling":
|
||||
return "Cancelling";
|
||||
case "completed":
|
||||
return "Completed";
|
||||
case "failed":
|
||||
return "Failed";
|
||||
case "cancelFailed":
|
||||
return "Cancel failed";
|
||||
case "cancelled":
|
||||
return "Cancelled";
|
||||
case "delivered":
|
||||
@ -107,16 +111,33 @@ function formatTaskStatus(status: BackgroundCompletion["status"]): string {
|
||||
|
||||
/** Terminal (finished) task states that carry a result summary (ticket #5). */
|
||||
function isTerminalTaskStatus(status: BackgroundCompletion["status"]): boolean {
|
||||
return status === "completed" || status === "failed" || status === "cancelled";
|
||||
return (
|
||||
status === "completed" ||
|
||||
status === "failed" ||
|
||||
status === "cancelFailed" ||
|
||||
status === "cancelled"
|
||||
);
|
||||
}
|
||||
|
||||
function taskStatusClass(status: BackgroundCompletion["status"]): string {
|
||||
if (status === "running" || status === "pending") return "bg-warning/15 text-warning";
|
||||
if (status === "running" || status === "pending" || status === "cancelling") {
|
||||
return "bg-warning/15 text-warning";
|
||||
}
|
||||
if (status === "completed" || status === "delivered") return "bg-success/10 text-success";
|
||||
if (status === "failed" || status === "cancelled") return "bg-danger/10 text-danger";
|
||||
if (status === "failed" || status === "cancelFailed" || status === "cancelled") {
|
||||
return "bg-danger/10 text-danger";
|
||||
}
|
||||
return "bg-raised text-muted";
|
||||
}
|
||||
|
||||
function canCancelTask(status: BackgroundCompletion["status"]): boolean {
|
||||
return status === "running" || status === "pending" || status === "cancelling";
|
||||
}
|
||||
|
||||
function canRetryTask(status: BackgroundCompletion["status"]): boolean {
|
||||
return status === "failed" || status === "cancelFailed" || status === "cancelled";
|
||||
}
|
||||
|
||||
function requesterLabel(ticket: AgentTicketState): string {
|
||||
if (ticket.source.kind === "human") return "Human";
|
||||
const id = shortTicket(ticket.source.agentId);
|
||||
@ -346,8 +367,8 @@ function BackgroundTaskRow({
|
||||
const attachmentRef = useRef<BackgroundTaskAttachment | null>(null);
|
||||
const attachSeqRef = useRef(0);
|
||||
const hasOutput = Boolean(task.stdoutTail || task.stderrTail);
|
||||
const canCancel = task.status === "running" || task.status === "pending";
|
||||
const canRetry = task.status === "failed" || task.status === "cancelled";
|
||||
const canCancel = canCancelTask(task.status);
|
||||
const canRetry = canRetryTask(task.status);
|
||||
const canAttachLive = canCancel;
|
||||
|
||||
function detachLive(): void {
|
||||
|
||||
Reference in New Issue
Block a user