fix(workflow-orchestration): correct resume and failure-branch routing in WorkflowRunner
- Resume no longer infinite-loops when a crash occurs between append_step_result() and update_run() for a BranchStep: the recorded branch target is now read from the persisted result instead of recomputed as None. - Resume no longer silently discards a recorded step failure that happened right before the crash; the run correctly ends failed. - A failed step's own next_step_id (e.g. routing to a BranchStep that evaluates step_result_success) is now honored instead of unconditionally forcing the run to failed, per spec. openspec: workflow-orchestration capability, archived change workflow-orchestration-runtime
This commit is contained in:
+36
-12
@@ -98,19 +98,21 @@ class WorkflowRunner:
|
||||
if self.step_limit is not None and executed >= self.step_limit:
|
||||
return run
|
||||
step = definition.step_by_id(run.current_step_id)
|
||||
if _already_recorded(run, step.step_id):
|
||||
next_step_id = self._next_step_id(definition, step, None)
|
||||
run = self._checkpoint(run, next_step_id, "running")
|
||||
recorded = _recorded_result(run, step.step_id)
|
||||
if recorded is not None:
|
||||
branch_next_step_id = None
|
||||
if isinstance(step, BranchStep):
|
||||
branch_next_step_id = recorded.detail.get("next_step_id")
|
||||
next_status, next_step_id = self._resolve_outcome(
|
||||
definition, step, recorded.success, branch_next_step_id
|
||||
)
|
||||
run = self._checkpoint(run, next_step_id, next_status)
|
||||
continue
|
||||
|
||||
result, branch_next_step_id = self._execute_step(definition, run, step)
|
||||
next_status = "running"
|
||||
next_step_id = self._next_step_id(definition, step, branch_next_step_id)
|
||||
if not result.success:
|
||||
next_status = "failed"
|
||||
next_step_id = None
|
||||
elif next_step_id is None:
|
||||
next_status = "completed"
|
||||
next_status, next_step_id = self._resolve_outcome(
|
||||
definition, step, result.success, branch_next_step_id
|
||||
)
|
||||
|
||||
self.store.append_step_result(run.id, result)
|
||||
run = self._checkpoint(
|
||||
@@ -121,6 +123,25 @@ class WorkflowRunner:
|
||||
executed += 1
|
||||
return run
|
||||
|
||||
def _resolve_outcome(
|
||||
self,
|
||||
definition: WorkflowDefinition,
|
||||
step: WorkflowStep,
|
||||
success: bool,
|
||||
branch_next_step_id: str | None,
|
||||
) -> tuple[str, str | None]:
|
||||
"""Compute the (status, next_step_id) for a step's outcome.
|
||||
|
||||
Shared by live execution and resume so a failed step's routing
|
||||
(e.g. to a branch step evaluating the failure) can never drift
|
||||
between the two code paths. The run only becomes terminal when
|
||||
there is truly no next step to route to.
|
||||
"""
|
||||
next_step_id = self._next_step_id(definition, step, branch_next_step_id)
|
||||
if next_step_id is None:
|
||||
return ("completed" if success else "failed"), None
|
||||
return "running", next_step_id
|
||||
|
||||
def _checkpoint(
|
||||
self,
|
||||
run: WorkflowRun,
|
||||
@@ -317,5 +338,8 @@ class WorkflowRunner:
|
||||
return definition.next_step_id_after(step.step_id)
|
||||
|
||||
|
||||
def _already_recorded(run: WorkflowRun, step_id: str) -> bool:
|
||||
return any(result.step_id == step_id for result in run.step_results)
|
||||
def _recorded_result(run: WorkflowRun, step_id: str) -> WorkflowStepResult | None:
|
||||
for result in reversed(run.step_results):
|
||||
if result.step_id == step_id:
|
||||
return result
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user