diff --git a/build/RetryLedger.cs b/build/RetryLedger.cs
index 342c1764e..31671f960 100644
--- a/build/RetryLedger.cs
+++ b/build/RetryLedger.cs
@@ -115,10 +115,41 @@ static FlakyFailure firstFailure(TestReport report)
ErrorType = string.IsNullOrWhiteSpace(outcome?.ErrorType) ? null : outcome.ErrorType,
// One line and bounded: this is a lead to open the log with, not a copy of it. A
// multi-line reason would also break the ::warning workflow command downstream.
- Reason = condense(outcome?.ErrorMessage)
+ Reason = condense(outcome?.ErrorMessage),
+ Stack = topFrames(outcome?.StackTrace)
};
}
+ ///
+ /// The top frames of the failing attempt's stack, for the ledger JSON only.
+ ///
+ /// The message says what went wrong; the stack is what says where, and for a whole
+ /// class of flake that is the only thing that distinguishes the candidates. The MQTT
+ /// Broken pipe flake is the worked example: the message alone is equally consistent with a
+ /// teardown ordering bug, a port collision between worker processes, and a keep-alive drop under
+ /// CI load. Three local experiments eliminated the first two and the third needs a call site, so
+ /// the investigation stalls on precisely this field.
+ ///
+ /// Deliberately kept out of the step summary, the annotation and the roll-up — all three are
+ /// glanceable surfaces and a stack would drown them. The JSON ledger is already a downloadable
+ /// artifact, which is the right place for something you go looking for on purpose.
+ ///
+ static string[] topFrames(string stackTrace)
+ {
+ if (string.IsNullOrWhiteSpace(stackTrace)) return [];
+
+ // Enough to cross a teardown/dispatch boundary and name the caller, not so much that the
+ // ledger becomes a copy of the log.
+ const int maxFrames = 12;
+
+ return stackTrace
+ .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim())
+ .Where(x => x.Length > 0)
+ .Take(maxFrames)
+ .ToArray();
+ }
+
///
/// Collapses a failure reason to a single bounded line, or null when there is nothing to say.
///
@@ -290,5 +321,11 @@ class FlakyFailure
/// The failing attempt's own error message, flattened to one bounded line.
public string Reason { get; init; }
+
+ ///
+ /// Top frames of the failing attempt's stack. Ledger JSON only — see
+ /// for why it is not rendered on the summary surfaces.
+ ///
+ public string[] Stack { get; init; } = [];
}
}