From 8aaf1f7e447cff9f015bf31ecd5842ccb3ffec86 Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Mon, 31 Aug 2026 10:56:15 +0700 Subject: [PATCH] #164: a backend-error failure must carry the whole scrape, not just the matched line 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. --- .../ltms/fleet/inject/CompletionResolver.java | 7 ++++++- .../fleet/inject/CompletionResolverTest.java | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) 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 417219e..94adcc9 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java +++ b/fleetd/src/main/java/dev/ltms/fleet/inject/CompletionResolver.java @@ -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; diff --git a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java index 9b150ef..3f7e3c0 100644 --- a/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java +++ b/fleetd/src/test/java/dev/ltms/fleet/inject/CompletionResolverTest.java @@ -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])",