Problem
RuntimeOrchestrationContext.setFailed() does not remove a previously added completeOrchestrationAction from _pendingActions before adding its own FAILED action. When setFailed() is called after setComplete() has already run (e.g., due to a NonDeterminismError thrown during replay after the generator finishes), the resulting getActions() returns two completeOrchestration actions — one COMPLETED and one FAILED — sent to the sidecar.
File: packages/durabletask-js/src/worker/runtime-orchestration-context.ts, setFailed() method (line ~226)
Root Cause
Both setComplete() and setContinuedAsNew() guard against double-completion with if (this._isComplete) { return; }. setFailed() has no such guard and no cleanup of the previous completion action.
The scenario:
- During replay, the generator finishes early (returns without yielding expected actions).
setComplete() is called via StopIterationError, adding a COMPLETED action to _pendingActions.
- A subsequent history event fails validation (e.g.,
handleTaskScheduled can't find its expected pending action), throwing NonDeterminismError.
- The outer
catch in execute() calls setFailed(error), which adds a SECOND completion action (FAILED) without removing the first.
getActions() returns both, producing an invalid response with two terminal actions.
Proposed Fix
When setFailed() is called and _isComplete is already true, find and remove the existing completeOrchestration action from _pendingActions before adding the FAILED action. This ensures exactly one completion action is returned, correctly reporting the failure.
Impact
Severity: Medium — Only triggers with non-deterministic orchestrator code during replay, but produces an invalid protocol response (duplicate completion actions) that could confuse the sidecar or cause undefined behavior.
Problem
RuntimeOrchestrationContext.setFailed()does not remove a previously addedcompleteOrchestrationActionfrom_pendingActionsbefore adding its own FAILED action. WhensetFailed()is called aftersetComplete()has already run (e.g., due to aNonDeterminismErrorthrown during replay after the generator finishes), the resultinggetActions()returns twocompleteOrchestrationactions — one COMPLETED and one FAILED — sent to the sidecar.File:
packages/durabletask-js/src/worker/runtime-orchestration-context.ts,setFailed()method (line ~226)Root Cause
Both
setComplete()andsetContinuedAsNew()guard against double-completion withif (this._isComplete) { return; }.setFailed()has no such guard and no cleanup of the previous completion action.The scenario:
setComplete()is called viaStopIterationError, adding a COMPLETED action to_pendingActions.handleTaskScheduledcan't find its expected pending action), throwingNonDeterminismError.catchinexecute()callssetFailed(error), which adds a SECOND completion action (FAILED) without removing the first.getActions()returns both, producing an invalid response with two terminal actions.Proposed Fix
When
setFailed()is called and_isCompleteis already true, find and remove the existingcompleteOrchestrationaction from_pendingActionsbefore adding the FAILED action. This ensures exactly one completion action is returned, correctly reporting the failure.Impact
Severity: Medium — Only triggers with non-deterministic orchestrator code during replay, but produces an invalid protocol response (duplicate completion actions) that could confuse the sidecar or cause undefined behavior.