audit: unify item write routing, fix serial interrupt loss, enforce result coherence

This commit is contained in:
lda
2026-09-04 11:20:15 +07:00 Verified
parent f155e6651a
commit 8a0737fe8b
9 changed files with 206 additions and 75 deletions
+12 -7
View File
@@ -341,13 +341,18 @@ def _finish_concurrent_foreach(
reducers: Mapping[str, ReducerDefinition] | None = None,
) -> RunState:
barrier = activation.barrier
error_records = [
result.error.to_metadata()
for result in sorted(
barrier.pending_results.values(), key=lambda item: item.index
)
if result.status == "failed" and result.error is not None
]
# Coherence is enforced at load, but re-check here: a failed result
# without an error must never silent-commit as `done`.
error_records = []
for result in sorted(barrier.pending_results.values(), key=lambda item: item.index):
if result.status != "failed":
continue
if result.error is None:
raise WorkflowExecutionError(
f"foreach item result for index {result.index!r} is failed "
"but carries no error"
)
error_records.append(result.error.to_metadata())
outcome = "completed_with_errors" if error_records else "done"
next_node_id = index.next_node_id(frame.node_id, outcome)
success_patches = [
+4 -2
View File
@@ -14,7 +14,7 @@ from wf_core.run_state import (
StepExecutionResult,
)
from wf_core.runtime.input_bindings import resolve_step_input_bindings
from wf_core.runtime.lineage import commit_patch_for_frame
from wf_core.runtime.lineage import commit_foreach_aware_patch
from wf_core.runtime.ops.flow import advance_frame, append_step_result_trace
from wf_core.runtime.ops.index import WorkflowIndex
from wf_core.runtime.ops.merges import ReducerDefinition
@@ -115,7 +115,9 @@ def resume_interrupt(
reducers=reducers,
missing_field_message="interrupt resume payload is missing required field {field}",
)
state_changes = commit_patch_for_frame(run, frame, patch)
# Foreach-aware routing: a serial item resume commits through the parent
# scope, a concurrent one buffers in the item lineage for barrier merge.
state_changes = commit_foreach_aware_patch(run, frame, patch)
next_node_id = index.next_node_id(frame.node_id, resume_outcome)
append_step_result_trace(
run,
+5 -33
View File
@@ -15,14 +15,9 @@ from wf_core.run_state import (
RuntimeContext,
StepExecutionResult,
)
from wf_core.runtime.foreach_state import (
item_frame_owner,
require_foreach_activation,
)
from wf_core.runtime.input_bindings import resolve_step_input_bindings
from wf_core.runtime.lineage import (
append_lineage_writes,
commit_patch_for_frame,
commit_foreach_aware_patch,
scope_input_for_frame,
)
from wf_core.runtime.ops.frames import frame_context_values
@@ -115,33 +110,10 @@ def _finalize_node_execution(
state_view,
reducers=reducers,
)
owner = item_frame_owner(frame)
if owner is None:
state_changes = commit_patch_for_frame(run, frame, patch)
else:
parent_frame = run.frames.get(owner.parent_frame_id)
if parent_frame is None:
raise WorkflowExecutionError(
"foreach item state references missing parent frame "
f"{owner.parent_frame_id!r} for child frame {frame.id!r}"
)
# Fail closed when the child names a closed or superseded activation:
# its writes must not land in a later visit's barrier.
activation = require_foreach_activation(
parent_frame, owner.foreach_node_id, owner.activation_id
)
if activation.barrier.mode == "concurrent":
# Concurrent writes stay buffered in the child lineage; the owner
# back-edge registers the completed item with the barrier.
append_lineage_writes(
run,
scope_id=frame.scope_id,
lineage_id=frame.lineage_id,
writes=patch.writes,
)
state_changes = {}
else:
state_changes = commit_patch_for_frame(run, parent_frame, patch)
# Foreach-aware routing (root, serial parent, concurrent lineage) is
# owned by the shared helper so every operation commits the same way.
# Closed or superseded activations fail closed inside.
state_changes = commit_foreach_aware_patch(run, frame, patch)
return StepExecutionResult(
outcome=result.outcome,
resolved_input=resolved_input,