diff --git a/fleetd/src/main/java/dev/ltms/fleet/inject/BackendErrorSink.java b/fleetd/src/main/java/dev/ltms/fleet/inject/BackendErrorSink.java index 3370afd..df6bd17 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/inject/BackendErrorSink.java +++ b/fleetd/src/main/java/dev/ltms/fleet/inject/BackendErrorSink.java @@ -1,10 +1,12 @@ package dev.ltms.fleet.inject; /** - * Notified when {@link CompletionResolver} actually delivers a typed backend-error classification - * to a waiting send (fleetd#201 / #227) — never on a race that lost. {@link CompletionResolver} - * calls this only after {@code Rendezvous.resolveFailure} returns {@code true} for that exact - * waiter, mirroring the win-only race rule {@link ExhaustionSink} already uses. + * Notified when {@link CompletionResolver} has a backend-error match at the start of a pane line, + * or both a match and its too-fast crash signature, for a waiting send (fleetd#201 / #227). A text + * match inside ordinary pane prose can be a member's report about an error, so it fails the send + * without notifying this sink. + * {@link CompletionResolver} calls this only after {@code Rendezvous.resolveFailure} returns + * {@code true} for that exact waiter, mirroring the win-only race rule {@link ExhaustionSink} uses. * *
The public send result is unchanged by this classification — it is still a failed send
* ({@code Rendezvous.Kind#FAILED}); this sink is the internal seam a later stage (fleetd#201 Unit
diff --git a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java
index c806fd8..a1787e6 100644
--- a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java
+++ b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java
@@ -387,9 +387,10 @@ public final class CompletionResolver implements TurnListener {
// fleetd#164 (part 2) / fleetd#201: a scrape that read cleanly and produced content still
// isn't a real reply when that content is the backend's own rejection (e.g. an HTTP 400
// before the worker did any work). Classify it as a failure naming the member, rather than
- // handing the caller a scrape that reads like a completed answer, and — only on the
- // resolution that actually wins the race, mirroring the exhaustion sink above — notify the
- // typed backend-error sink so a later stage can act on repeated failures.
+ // handing the caller a scrape that reads like a completed answer. A text match alone is not
+ // enough to notify the typed backend-error sink: this assistant block can be a member's
+ // normal prose about an error. A line that starts with the error match is stronger evidence;
+ // the too-fast path below also has its crash signature before it records a credential failure.
String backendError = firstMatchingLine(assistantBlock, backendErrorPatternOrFallback(target));
if (backendError != null) {
// Carry the whole scrape, not just the matched line. The pattern is a heuristic: a member
@@ -401,9 +402,9 @@ public final class CompletionResolver implements TurnListener {
if (rendezvous.resolveFailure(waiter, reason)) {
inFlight.remove(target, turn);
log.warn("failing send to {} via turn-stall fallback: {}", target, reason);
- // fleetd#201 Unit 1: only on the resolution that actually won the race — a late
- // duplicate must never double-count one backend failure.
- backendErrorSink.onBackendError(target, backendError, reason);
+ if (startsWithBackendError(backendError, backendErrorPatternOrFallback(target))) {
+ backendErrorSink.onBackendError(target, backendError, reason);
+ }
}
return;
}
@@ -492,8 +493,9 @@ public final class CompletionResolver implements TurnListener {
if (rendezvous.resolveFailure(waiter, reason)) {
inFlight.remove(target, turn);
log.warn("failing send to {} via turn-stall fallback from the raw scrape: {}", target, reason);
- // fleetd#201 Unit 1: only on the resolution that actually won the race.
- backendErrorSink.onBackendError(target, backendError, reason);
+ if (startsWithBackendError(backendError, backendErrorPatternOrFallback(target))) {
+ backendErrorSink.onBackendError(target, backendError, reason);
+ }
}
return true;
}
@@ -540,10 +542,10 @@ public final class CompletionResolver implements TurnListener {
* {@link #MIN_TURN_NANOS} — a crash signature (e.g. a backend HTTP 400 before the worker did
* anything) that a bare {@code BUSY -> DONE} transition cannot be told apart from a genuinely
* fast completion. Runs the same backend-error classification the normal and raw-scrape paths
- * apply, against whatever is on screen right now: a match is a typed failure that notifies
- * {@link #backendErrorSink} (only on the resolution that wins the race); a non-match stays the
- * original generic too-fast failure, naming the member and both timings, with whatever the pane
- * shows appended so the caller sees the cause, not just "it failed".
+ * apply, against whatever is on screen right now: a match together with the too-fast crash
+ * signature notifies {@link #backendErrorSink} (only on the resolution that wins the race). A
+ * non-match stays the original generic too-fast failure, naming the member and both timings,
+ * with whatever the pane shows appended so the caller sees the cause, not just "it failed".
*/
private void failTooFast(String target, InFlight turn, CompletableFuture