A Fatal flow step emits its ERROR line once at its own step boundary and again at every ancestor wrapper node, because status_or_error() returns Err for Fatal and the ? in the call macros re-surfaces the identical ErrorWithStepStatus at the parent. Verified 2026-09-02 against mando origin/develop tip b5770766 and prod tag v1.17.1+2757320894.7c1ef44f (7c1ef44f).
Still live on develop
Symptoms (prod Datadog)
One incident, exec_id 5937b3e7-d39f-44d0-bdd0-fd8dc721b217, produced two ERROR events from the same emission site mando-lib/src/workflow/mod.rs:361, differing only in step path:
| Step path | flow.step.execution_time | Fingerprint |
|---|---|---|
data_update.load_battery_timeseries | 1.809s | ...InternalDuckDbError|data_update.load_battery_timeseries |
data_update | 5.783s | ...InternalDuckDbError|data_update |
Because error.fingerprint embeds the step path, the two copies do not aggregate: one root cause shows up as two distinct fingerprints in Datadog.
Timing confirmation that the second event is the wrapper waiting on its slowest child: tokio::join! makes the wrapper finish with the slowest sibling; log gap 3.969s vs exec_time gap 3.974s, a match within 5ms.
Root cause
Three pieces, all in mando-lib/src/workflow/:
run_step(mod.rs:465-540) callsstate.log()unconditionally at line 499 for every step it executes, wrapper nodes included.call_parallel!(macros.rs,@unwraparm at line 120) runs$step?.status_or_error()?.status_or_error()(mod.rs:382-388) returnsOk(status)whenis_error_or_better()(Skip / Success / Warning / Error), butErr(err)for Fatal, returning the identicalErrorWithStepStatus.
pub fn status_or_error(self) -> Result<StepStatus, ErrorWithStepStatus> {
match self.result {
Ok(result) => Ok(result.step_status.clone()),
Err(err) if err.status.is_error_or_better() => Ok(err.status),
Err(err) => Err(err),
}
}The ? propagates the Fatal error out of the generated flow’s call(), so the wrapper step (flow: Wrapper, the desc on WorkflowStep::SequentialSteps) finishes with the same error object and re-logs it through the same line 499.
For Agents
Blast radius facts:
- Fires only for Fatal. Error status is swallowed into
Okbystatus_or_error()and never propagates, so it emits once.- Copy count = 1 leaf + one per ancestor node carrying a
desc. Deeper flow trees emit more than 2 copies.- NOT parallel-group specific: the sequential call macros (
macros.rslines 11, 20, 34, 45) use the samestatus_or_error()?.
Fix constraints
The Fatal → Err propagation is intentional and pinned by two tests:
status_or_error_preserves_error_status_as_ok(mod.rs:998)status_or_error_propagates_fatal_as_err(mod.rs:1005)
So the fix belongs at the emission decision, not at the propagation. It must also not reintroduce a logged_at_site-style flag, since BE-3541 deliberately removed exactly that mechanism.
Suggested shape: only WorkflowStep::Step emits the error payload; wrapper nodes (SequentialSteps / parallel group descriptors) emit status only.
Underlying trigger in that prod log: an already-fixed bug
The failure being double-logged was a DuckDB duplicate primary key on (data_point_id, value_time, generation_time).
- That is precisely what MR !610 (BE-2132) fixes via
dab6cba6: it replaces the hardcoded(data_point_id, value_time)dedup grouping/join with the fullschema.pk_columns. git merge-base --is-ancestorconfirmsdab6cba6andf418e9b3are NOT in prod7c1ef44f, but ARE on develop and on the test image336ce49f.
Prod is hitting an already-fixed bug
The duplicate-PK crash in prod is fixed on develop/test but not in the deployed prod build. The double-logging defect is separate and unfixed everywhere.
Related
- BE-3541 Single Error Emission - removed detection-site double logging; this is the ancestor-boundary axis it did not cover
- BE-4067 error DX clarity - MR !601, confirmed not to touch the emission path
- BE-4067 whole-mando error unwalling
- flow-step-log-message-dropped-2026-05-26 - earlier
status_or_error/StepResult::logdefect in the same code - BE-3482 Datadog Logs and APM Conformance
- BE-3117 Per-Flow Error Context Store
- pymando-rust-log-trace-correlation-gap-2026-09-02 - sibling finding from the same 2026-09-02 pass
- mando-lib
- Agent Context