Merge task branch for: fix(worker): deleting the last non-terminal child leaves the parent stuck in WaitingForChildren

This commit is contained in:
mika kuns
2026-07-23 18:00:48 +02:00
5 changed files with 36 additions and 7 deletions
+2
View File
@@ -357,6 +357,8 @@ public sealed class ExternalMcpService
throw new InvalidOperationException("Cannot delete a running task. Cancel it first.");
await _tasks.DeleteAsync(taskId, cancellationToken);
if (task.ParentTaskId is not null)
await _state.TryAdvanceParentAsync(task.ParentTaskId);
await _broadcaster.TaskUpdated(taskId);
return new DeleteTaskResult(true, taskId);
}
@@ -24,5 +24,10 @@ public interface ITaskStateService
Task<TransitionResult> BlockOnAsync(string taskId, string predecessorTaskId, CancellationToken ct);
Task<TransitionResult> UnblockAsync(string taskId, CancellationToken ct);
// Surfaces a WaitingForChildren parent for review once all its children are terminal.
// Best-effort (swallows and logs failures) — safe to call after any child mutation,
// e.g. deleting the last non-terminal child (no terminal transition fires for a delete).
Task TryAdvanceParentAsync(string parentId);
Task<int> RecoverStaleRunningAsync(string reason, CancellationToken ct);
}
+11 -5
View File
@@ -417,9 +417,18 @@ public sealed class TaskStateService : ITaskStateService
_logger.LogWarning(ex, "PlanningChain advance failed for {TaskId}", taskId);
}
await TryAdvanceParentAsync(parentId);
}
// Any parent (planning or improvement) sitting in WaitingForChildren surfaces for review
// once every child is terminal (Done/Failed/Cancelled). A failed or cancelled child does
// not wedge the parent — it is flagged on the result. Also called directly after a child
// is deleted, since no terminal transition fires in that case.
public async Task TryAdvanceParentAsync(string parentId)
{
try
{
await TryAdvanceParentAsync(parentId);
await AdvanceParentIfAllChildrenTerminalAsync(parentId);
}
catch (Exception ex)
{
@@ -427,10 +436,7 @@ public sealed class TaskStateService : ITaskStateService
}
}
// Any parent (planning or improvement) sitting in WaitingForChildren surfaces for review
// once every child is terminal (Done/Failed/Cancelled). A failed or cancelled child does
// not wedge the parent — it is flagged on the result.
private async Task TryAdvanceParentAsync(string parentId)
private async Task AdvanceParentIfAllChildrenTerminalAsync(string parentId)
{
string? parentResult;
List<TaskStatus> childStatuses;