Skip to content

Commit 4b3edd9

Browse files
fix: loop node reports iteration failures as partial success (#518)
Co-authored-by: Wei Lin <wei@avaprotocol.org>
1 parent 272c320 commit 4b3edd9

2 files changed

Lines changed: 40 additions & 11 deletions

File tree

core/taskengine/manual_trigger_loop_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -426,14 +426,15 @@ func TestLoopNode_ContractWrite_InvalidAddress_PartialFailure(t *testing.T) {
426426
node.Name = "loopTransfer"
427427

428428
step, err := vm.RunNodeWithInputs(node, inputVariables)
429+
require.NoError(t, err, "loop infrastructure should not fail")
429430

430-
// Per AvaProtocol/EigenLayer-AVS#511, per-iteration runner failures do not
431-
// fail the loop step. The loop ran to completion, so step.Success is true
432-
// and the per-iteration outcomes are reflected in the data array (nil for
433-
// failed iterations).
431+
// When some iterations fail, the loop step reports success=false with a
432+
// descriptive error so that AnalyzeExecutionResult detects partial_success.
433+
// The loop still ran to completion and preserves OutputData with nil entries
434+
// for failed iterations.
434435
require.NotNil(t, step, "Execution step should not be nil")
435-
assert.True(t, step.Success, "Loop step should succeed when it ran to completion, even if an iteration failed")
436-
assert.Empty(t, step.Error, "Loop step error should be empty when loop ran to completion")
436+
assert.False(t, step.Success, "Loop step should report failure when iterations failed")
437+
assert.Contains(t, step.Error, "1 of 2 iterations failed", "Error should describe the partial failure")
437438

438439
// The output should still contain results (first iteration data, second nil)
439440
loopOutput := step.GetLoop()

core/taskengine/vm.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4840,16 +4840,44 @@ func (v *VM) executeLoopWithQueue(stepID string, taskNode *avsproto.TaskNode, no
48404840
}
48414841

48424842
// Only treat infrastructure failures (queue submit errors, iteration timeouts)
4843-
// as a hard step failure. Per-iteration runner errors (e.g. a contract call
4844-
// reverting in one iteration of a Loop > ContractRead) are reflected as nil
4845-
// entries in the results array — the loop ran to completion, so we preserve
4846-
// OutputData and return success so the client can inspect partial results.
4847-
// See AvaProtocol/EigenLayer-AVS#511.
4843+
// as a hard step failure.
48484844
if infraFailure && firstError != nil {
48494845
finalizeStep(s, false, nil, firstError.Error(), log.String())
48504846
return s, firstError
48514847
}
48524848

4849+
// Count successful vs failed iterations to determine step status.
4850+
// Per-iteration runner errors (e.g. a contract call reverting) are reflected
4851+
// as nil entries in the results array. The loop ran to completion, so we
4852+
// always preserve OutputData for the client to inspect partial results.
4853+
// See AvaProtocol/EigenLayer-AVS#511.
4854+
iterationFailCount := 0
4855+
for _, result := range results {
4856+
if result == nil {
4857+
iterationFailCount++
4858+
}
4859+
}
4860+
4861+
if iterationFailCount > 0 && firstError != nil {
4862+
// Some or all iterations failed — mark the loop step as failed so
4863+
// AnalyzeExecutionResult can detect partial_success at the execution level.
4864+
// Pass the error via the `err` parameter (not `errorMessage`) so that
4865+
// finalizeStep uses err.Error() directly without wrapping it in
4866+
// NewInvalidRequestError which adds an "invalid request: " prefix.
4867+
innerMsg := strings.TrimPrefix(firstError.Error(), "invalid request: ")
4868+
errorMsg := fmt.Sprintf("%d of %d iterations failed: %s", iterationFailCount, len(results), innerMsg)
4869+
loopErr := NewStructuredError(
4870+
avsproto.ErrorCode_INVALID_REQUEST,
4871+
errorMsg,
4872+
map[string]interface{}{
4873+
"failed_iterations": iterationFailCount,
4874+
"total_iterations": len(results),
4875+
},
4876+
)
4877+
finalizeStep(s, false, loopErr, "", log.String())
4878+
return s, nil // return nil error: the loop itself ran to completion
4879+
}
4880+
48534881
finalizeStep(s, success, nil, "", log.String())
48544882
return s, nil
48554883
}

0 commit comments

Comments
 (0)