A container activity had no way to learn that one of its children faulted.
ExceptionHandlingMiddleware caught the exception, called context.Fault(e) and
handed off to the workflow-global IIncidentStrategy; the container's completion
callback never fired, because the child never completed.
Add a seam on the ancestor-bubbling signal channel that already exists:
- FaultSignal(Exception, ActivityExecutionContext), beside CancelSignal. Its XML
doc carries the contract, including why a handler must not call
RecoverFromFault and why the CompleteActivityAsync sweep is a backstop rather
than the mechanism.
- An internal bool-returning TrySendSignalAsync, since SignalContext
.StopPropagationRequested is internal and SendSignalAsync reported nothing.
SendSignalAsync keeps its public signature and delegates to it.
- ExceptionHandlingMiddleware sends the signal after faulting and, when an
ancestor stops propagation, calls RecoverFromFault once and returns instead of
raising an incident.
RecoverFromFault now transitions to Running only when the activity is still
Faulted. It is called after the handler runs, so the unconditional transition
would otherwise undo a handler that cancelled or completed the faulted child.
The counts are still reset unconditionally, and the one pre-existing caller is
unaffected.
Behavior is unchanged when nobody handles the signal: verified by running the
new unhandled-fault theory against the pre-change middleware, and by
IncidentStrategyTests and Primitives/FaultTests passing unmodified.
Refs #7911
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>