Repository navigation
.NET: Release the workflow when a run ends after an executor fails to initialize - #9585
Open
Ruiqi Wang (RachelWanggg) wants to merge 2 commits into
Conversation
… initialize EndRunAsync awaited every executor creation task before releasing ownership, so a factory or InitializeAsync that threw or was cancelled made disposing the run rethrow that error and left the Workflow owned: every later run on it failed with "already owned by another runner". Skip executors that were never created, and release ownership in a finally so a failing executor dispose cannot keep it either. RunStreamingAsync and RunAsync also dropped the run handle when enqueueing the first input threw (the start executor is created there), so the run was never ended. Dispose the handle before rethrowing.
Ruiqi Wang (RachelWanggg)
deployed
to
github-app-auth
October 21, 2026 17:18 — with
GitHub Actions
Active
Ruiqi Wang (RachelWanggg)
deployed
to
github-app-auth
October 21, 2026 17:18 — with
GitHub Actions
Active
Vincent Biret (baywet)
approved these changes
Oct 21, 2026
Vincent Biret (baywet)
left a comment
Member
There was a problem hiding this comment.
Thanks for the contribution!
Vincent Biret (baywet)
enabled auto-merge
October 21, 2026 17:50
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Oct 21, 2026
Vincent Biret (baywet)
deployed
to
github-app-auth
October 23, 2026 13:26 — with
GitHub Actions
Active
Vincent Biret (baywet)
enabled auto-merge
October 23, 2026 13:27
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Oct 23, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation & Context
A
Workflowthat hits one executor initialization failure can never run again.InProcessRunnerContext.EndRunAsyncawaits every cached executor creation task before it releases ownership, so when an executor'sInitializeAsync(or factory) threw, or was cancelled because the run was disposed while it was still initializing, ending the run rethrows that error and skipsReleaseOwnershipAsync.Run.DisposeAsync/StreamingRun.DisposeAsyncthrows the old error, and every later run on the same instance fails with "Cannot use a Workflow that is already owned by another runner or parent workflow."RunStreamingAsyncandRunAsynchave a second path to the same state: they take ownership, then enqueue the first input, which can create the start executor. If that throws, the run handle is dropped without being disposed, so the run never ends.Workflows are usually built once and reused (
AddWorkflowregisters a singleton by default), so a transient failure while an executor connects to something inInitializeAsync, or a request abandoned while an executor is still initializing, leaves the workflow unusable until the process restarts.Description & Review Guide
EndRunAsyncskips executors whose creation task faulted or was cancelled, since there is no instance to dispose and the failure was already reported while the run executed, and releases ownership in afinally.RunStreamingAsyncandBeginRunHandlingChatProtocolAsync(used byRunAsync) dispose the run handle before rethrowing when enqueueing the first input fails, because the caller never receives that run and cannot end it.ExecutorFailedEvent/WorkflowErrorEvent, or thrown byRunStreamingAsync/RunAsync. If an executor's ownDisposeAsyncthrows, that exception still propagates from disposing the run, as before, but the workflow is now released. Runs without creation failures end exactly as before. No public API change.EndRunAsyncshould skip a faulted creation task, as here, or still surface the error. The error has already reached the caller by the time the run is disposed, and rethrowing it fromDisposeAsyncis what kept the workflow owned.Tests in
InProcessExecutionTests.cs, all failing onmain(the theories run on Lockstep and OffThread):RunAsync_ReleasesWorkflowAfterExecutorInitializationFailsAsync: the second executor'sInitializeAsyncthrows on the first run. The run reports anExecutorFailedEvent, disposing it does not throw, and the next run yields its output. Onmain, disposing throws theTimeoutException.StreamingRun_ReleasesWorkflowWhenDisposedDuringExecutorInitializationAsync: the run is disposed while the second executor'sInitializeAsyncwaits on its cancellation token, then the workflow runs again. Onmain, disposing throwsTaskCanceledException.RunAsync_ReleasesWorkflowAfterExecutorDisposalFailsAsync: the executor'sDisposeAsyncthrows once. Disposing the run throws that exception, and the next run still works. Onmainit fails with "already owned".RunStreamingAsync_ReleasesWorkflowAfterStartExecutorInitializationFailsAsync: the start executor fails to initialize whileRunStreamingAsyncenqueues the first input. The call throws, and the next call works. Onmainit fails with "already owned".RunAsync_ReleasesWorkflowAfterStartExecutorInitializationFailsAsync: the same throughRunAsync, with an input type the start executor does not declare, so the start executor is created while enqueueing. Onmainit fails with "already owned".Local verification on macOS arm64, .NET SDK 10.0.402,
dotnet test --project tests/<project> -f net10.0, branch on top of 0200599. Themaincolumn is a clean checkout of 0200599 with only the newInProcessExecutionTests.cs.Microsoft.Agents.AI.<name>.UnitTests)mainSkip)Skip)InProcessExecutionTestsonly: awaiting the creation task without the catch fails the initialization and cancellation tests. Releasing ownership only after a clean loop (nofinally) failsRunAsync_ReleasesWorkflowAfterExecutorDisposalFailsAsync. Skipping only faulted, not cancelled, creation tasks failsStreamingRun_ReleasesWorkflowWhenDisposedDuringExecutorInitializationAsync. Removing the dispose fromRunStreamingAsyncor fromBeginRunHandlingChatProtocolAsyncfails the matching start executor test. Not caught:breakinstead ofcontinue, which would stop disposing the executors after the failed one. Which executors come after it depends onConcurrentDictionaryorder, so the tests do not pin it.dotnet format <csproj> --verify-no-changespasses forMicrosoft.Agents.AI.WorkflowsandMicrosoft.Agents.AI.Workflows.UnitTests.dotnet build src/Microsoft.Agents.AI.Workflows --warnaserrorpasses for net10.0, net9.0, net8.0, netstandard2.0 and net472 with 0 warnings, and so does the unit test project for net10.0 and net472.net472tests (Windows only), integration tests, and unit test projects that do not depend onMicrosoft.Agents.AI.Workflows.Related Issue
Fixes #9584
AI Assistance
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix). A workflow keeps the label and title prefix in sync automatically.