HandleException classifies in order: exception is ModuleTimeoutException → IsTimeout(...) → IsPipelineCancelled(...) → Failed. IsPipelineCancelled includes or ModuleTimeoutException in its type test (line 752), but that arm is unreachable — the first branch consumes every ModuleTimeoutException, so a module that times out after the pipeline has already been cancelled is classified TimedOut and re-enters CancelPipelineAndThrow rather than returning a PipelineTerminated result as the dead branch intends.
Separately, because IsTimeout (elapsed >= timeout && OCE) is evaluated before IsPipelineCancelled, an externally-cancelled module whose elapsed time happens to exceed its timeout (TimeoutHelper prioritizes external cancellation at TimeoutHelper.cs:146-149, so this race is real) is recorded as TimedOut/Failed instead of PipelineTerminated.
Fix: rework the chain into a single explicit decision table: pipeline-cancelled first, then own-timeout, then failure — and delete the dead type test.
Evidence: src/ModularPipelines/Engine/ModuleExecutionPipeline.cs:665-698,738-754
Raised by the v4 scoping audit round 3 — umbrella #3239.
HandleExceptionclassifies in order:exception is ModuleTimeoutException→IsTimeout(...)→IsPipelineCancelled(...)→ Failed.IsPipelineCancelledincludesor ModuleTimeoutExceptionin its type test (line 752), but that arm is unreachable — the first branch consumes everyModuleTimeoutException, so a module that times out after the pipeline has already been cancelled is classified TimedOut and re-entersCancelPipelineAndThrowrather than returning a PipelineTerminated result as the dead branch intends.Separately, because
IsTimeout(elapsed >= timeout && OCE) is evaluated beforeIsPipelineCancelled, an externally-cancelled module whose elapsed time happens to exceed its timeout (TimeoutHelper prioritizes external cancellation atTimeoutHelper.cs:146-149, so this race is real) is recorded as TimedOut/Failed instead of PipelineTerminated.Fix: rework the chain into a single explicit decision table: pipeline-cancelled first, then own-timeout, then failure — and delete the dead type test.
Evidence:
src/ModularPipelines/Engine/ModuleExecutionPipeline.cs:665-698,738-754Raised by the v4 scoping audit round 3 — umbrella #3239.