#164: a backend-error failure must carry the whole scrape, not just the matched line
CI / contract (pull_request) Successful in 1m18s
CI / build (pull_request) Successful in 1m28s

The BACKEND_ERROR pattern is a heuristic. It also matches a member that forgot
fleet_reply while reporting *about* a backend error. Failing is still right --
the caller must never read a scrape as an answer -- but the reason carried only
the matched line, so the rest of the report was thrown away.

That is the same defect #164 exists to fix: information destroyed on the way to
the caller. Carry the full pane tail alongside the classification, so a genuine
backend error reads the same as before and a false positive keeps its report.
This commit is contained in:
Dai Ha
2026-08-31 10:56:15 +07:00
parent 1fcda74596
commit 8aaf1f7e44
2 changed files with 27 additions and 1 deletions
@@ -292,7 +292,12 @@ public final class CompletionResolver implements TurnListener {
// scrape that reads like a completed answer.
String backendError = firstMatchingLine(assistantBlock, BACKEND_ERROR);
if (backendError != null) {
fail(target, turn, "member " + target + " ended on a backend error: " + backendError);
// Carry the whole scrape, not just the matched line. The pattern is a heuristic: a member
// that forgot fleet_reply while reporting *about* a backend error matches it too. Failing
// is still right — the caller must not read a scrape as an answer — but dropping the rest
// of the pane would destroy the report, which is the same defect fleetd#164 is about.
fail(target, turn, "member " + target + " ended on a backend error: " + backendError
+ "\n--- pane tail ---\n" + tail);
return;
}
String completion = clipped ? tail + "\n" + CLIPPED_PANE_TAIL_MARKER : tail;
@@ -612,6 +612,27 @@ class CompletionResolverTest {
assertEquals(Rendezvous.Kind.FAILED, waiter.getNow(null).kind(), "the pattern is case-insensitive");
}
@Test
void aBackendErrorFailureStillCarriesTheRestOfTheScrape() {
// The pattern is a heuristic: a member that forgot fleet_reply while *reporting on* a backend
// error matches it too. Failing is still correct, but the report itself must survive — losing
// it would be the same information-destroying defect fleetd#164 exists to fix.
String block = "\u23fa I looked into the gateway problem.\n"
+ "The log line was: API Error: 400 invalid request body\n"
+ "The cause is a missing content-type header.\n\u276f ";
FakeHerdr herdr = new FakeHerdr().readText(block);
Rendezvous rendezvous = new Rendezvous();
CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous, ExhaustedPatternLookup.none(), ExhaustionSink.none());
var waiter = rendezvous.open("term_a");
resolver.resolve("term_a", new CompletionResolver.InFlight(waiter, null));
String reason = waiter.getNow(null).text();
assertEquals(Rendezvous.Kind.FAILED, waiter.getNow(null).kind());
assertTrue(reason.contains("The cause is a missing content-type header."),
"the failure carries the rest of the pane, not only the matched line: " + reason);
}
@Test
void coverageIsOffWhenNoProfileHasAPatternConfigured() {
assertEquals("off (no profile has an exhaustedPattern configured; profiles: [terra])",