{"record":{"id":"3a72a934fadb33d2","repo":"microsoft/aspire","slug":"cannot-complete-task-task-id-because-its-parent-step-task","errorCode":null,"errorMessage":"Cannot complete task '{task.Id}' because its parent step '{task.StepId}' is already complete.","messagePattern":"Cannot complete task '(.+?)' because its parent step '(.+?)' is already complete\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs","lineNumber":225,"sourceCode":"\n    public async Task CompleteTaskAsync(ReportingTask task, CompletionState completionState, string? completionMessage, bool enableMarkdown, CancellationToken cancellationToken)\n    {\n        if (!_steps.TryGetValue(task.StepId, out var parentStep))\n        {\n            throw new InvalidOperationException($\"Parent step with ID '{task.StepId}' does not exist.\");\n        }\n\n        // If the task is already in a terminal state, this is a noop (idempotent)\n        if (task.CompletionState != CompletionState.InProgress)\n        {\n            return;\n        }\n\n        lock (parentStep)\n        {\n            if (parentStep.CompletionState != CompletionState.InProgress)\n            {\n                throw new InvalidOperationException($\"Cannot complete task '{task.Id}' because its parent step '{task.StepId}' is already complete.\");\n            }\n\n            task.CompletionState = completionState;\n            task.CompletionMessage = completionMessage ?? string.Empty;\n        }\n\n        var state = new PublishingActivity\n        {\n            Type = PublishingActivityTypes.Task,\n            Data = new PublishingActivityData\n            {\n                Id = task.Id,\n                StatusText = task.StatusText,\n                CompletionState = ToBackchannelCompletionState(completionState),\n                StepId = task.StepId,\n                CompletionMessage = completionMessage,\n                EnableMarkdown = enableMarkdown\n            }","sourceCodeStart":207,"sourceCodeEnd":243,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting/Pipelines/PipelineActivityReporter.cs#L207-L243","documentation":"CompleteTaskAsync enforces that the parent step is still InProgress when a task is finalized: completing a task against an already-completed step would produce incoherent reported state. The check happens under the step lock; if CompletionState != InProgress, InvalidOperationException is thrown. (The task's own idempotent-completion check runs earlier and returns silently for repeat task completion.)","triggerScenarios":"Calling CompleteTaskAsync after the parent step already completed — e.g. error handlers completing the step first, then cleanup code completing its tasks; concurrent completion racing between step and task owners.","commonSituations":"Exception filters that complete the step on failure while the task body also completes the task in a finally block; parallel tasks completing after the step-level error path; tests completing steps eagerly.","solutions":["Complete tasks before completing their parent step; make step completion the final operation.","Complete the step only after all child tasks reach a terminal state (await each CompleteTaskAsync).","In finally/cleanup blocks, check task.CompletionState == CompletionState.InProgress before completing.","Catch InvalidOperationException for best-effort completion where racing completion is acceptable."],"exampleFix":"// before\nawait step.CompleteAsync(CompletionState.Completed, null, false);\nawait reporter.CompleteTaskAsync(task, CompletionState.Completed, null, false); // throws\n// after\nawait reporter.CompleteTaskAsync(task, CompletionState.Completed, null, false);\nawait step.CompleteAsync(CompletionState.Completed, null, false);","handlingStrategy":"validation","validationCode":"if (task.CompletionState == CompletionState.InProgress)\n    await reporter.CompleteTaskAsync(task, state, msg, false);","typeGuard":"bool CanComplete(ReportingTask task) => task.CompletionState == CompletionState.InProgress;","tryCatchPattern":"try { await reporter.CompleteTaskAsync(task, state, msg, false); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"already complete\")) { logger.LogDebug(\"Step already complete; task result not recorded.\"); }","preventionTips":["Always complete child tasks before completing the step","Await CompleteTaskAsync in the task body, not in a late finally that races step completion","Make step completion the last operation of the step","For best-effort cleanup, tolerate the thrown InvalidOperationException explicitly"],"tags":["reporting","lifecycle","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T09:17:21.228Z"}