From d04b075996d3c724e4373997ac84a93a2a1781ad Mon Sep 17 00:00:00 2001 From: Dai Ha Date: Sat, 15 Aug 2026 04:25:54 +0200 Subject: [PATCH] CB-563: mark clipped completion scrapes --- .../bridged/inject/CompletionResolver.java | 18 +++++++++-- .../inject/CompletionResolverTest.java | 30 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/bridged/src/main/java/dev/ltms/bridged/inject/CompletionResolver.java b/bridged/src/main/java/dev/ltms/bridged/inject/CompletionResolver.java index 87fcf6c..d97a5bf 100644 --- a/bridged/src/main/java/dev/ltms/bridged/inject/CompletionResolver.java +++ b/bridged/src/main/java/dev/ltms/bridged/inject/CompletionResolver.java @@ -55,6 +55,9 @@ public final class CompletionResolver implements TurnListener { /** Cap the scraped tail so a long transcript can't return an unbounded blob. */ static final int MAX_SCRAPE_CHARS = 4000; + private static final String CLIPPED_PANE_TAIL_MARKER = + "[Pane tail clipped: member did not call bridge_reply.]"; + private final AgentControl agents; private final Rendezvous rendezvous; @@ -147,9 +150,14 @@ public final class CompletionResolver implements TurnListener { return; } String tail; + int originalLength = 0; + boolean clipped = false; boolean scrapeFailed = false; try { - tail = clip(lastAssistantBlock(agents.read(target, SCRAPE_SOURCE))); + String assistantBlock = lastAssistantBlock(agents.read(target, SCRAPE_SOURCE)); + originalLength = assistantBlock.strip().length(); + clipped = originalLength > MAX_SCRAPE_CHARS; + tail = clip(assistantBlock); } catch (RuntimeException e) { // The worker finished but we couldn't read its screen — still resolve the send so the // caller unblocks; an empty tail beats hanging until the caller's timeout. @@ -169,8 +177,14 @@ public final class CompletionResolver implements TurnListener { target); return; // keep the in-flight record: a later genuine completion still needs it } - if (rendezvous.resolveCompletion(waiter, tail)) { + String completion = clipped ? tail + "\n" + CLIPPED_PANE_TAIL_MARKER : tail; + if (rendezvous.resolveCompletion(waiter, completion)) { inFlight.remove(target, turn); + if (clipped) { + log.warn("completion scrape for {} clipped from {} chars to the {} char cap; " + + "member did not call bridge_reply, so the pane tail is partial", + target, originalLength, MAX_SCRAPE_CHARS); + } log.debug("resolved send to {} via turn-completion fallback ({} chars scraped)", target, tail.length()); } diff --git a/bridged/src/test/java/dev/ltms/bridged/inject/CompletionResolverTest.java b/bridged/src/test/java/dev/ltms/bridged/inject/CompletionResolverTest.java index 8462072..c7dd2fe 100644 --- a/bridged/src/test/java/dev/ltms/bridged/inject/CompletionResolverTest.java +++ b/bridged/src/test/java/dev/ltms/bridged/inject/CompletionResolverTest.java @@ -156,6 +156,33 @@ class CompletionResolverTest { assertEquals("No, 391 = 17 × 23.", waiter.getNow(null).text()); } + @Test + void marksAClippedCompletionPaneTail() { + String block = "⏺ " + "x".repeat(CompletionResolver.MAX_SCRAPE_CHARS + 1) + "\n❯ "; + FakeHerdr herdr = new FakeHerdr().readText(block); + Rendezvous rendezvous = new Rendezvous(); + CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous); + + var waiter = rendezvous.open("term_a"); + resolver.resolve("term_a", new CompletionResolver.InFlight(waiter, null)); + + assertEquals("x".repeat(CompletionResolver.MAX_SCRAPE_CHARS) + + "\n[Pane tail clipped: member did not call bridge_reply.]", + waiter.getNow(null).text()); + } + + @Test + void leavesAnUnclippedCompletionPaneTailUnmarked() { + FakeHerdr herdr = new FakeHerdr().readText("⏺ complete report\n❯ "); + Rendezvous rendezvous = new Rendezvous(); + CompletionResolver resolver = new CompletionResolver(new AgentControl(herdr), rendezvous); + + var waiter = rendezvous.open("term_a"); + resolver.resolve("term_a", new CompletionResolver.InFlight(waiter, null)); + + assertEquals("complete report", waiter.getNow(null).text()); + } + @Test void resolvesSynchronouslyBeforePostTurnContextClearing() { FakeHerdr herdr = new FakeHerdr().readText("⏺ previous answer\n❯ "); @@ -177,7 +204,8 @@ class CompletionResolverTest { // block while resolve compares against a clip()'d tail. For a block longer than MAX_SCRAPE_CHARS // the two capped representations differ even when the pane never changed, so the CB-115 // byte-identical guard failed to fire and a stale completion could resolve the send. Both sides - // must clip identically; here an unchanged >cap block on rapid back-to-back turns stays suppressed. + // must clip identically. The returned-text marker is added only after this comparison, so an + // unchanged >cap block on rapid back-to-back turns still stays suppressed. String longBlock = "⏺ " + "x".repeat(CompletionResolver.MAX_SCRAPE_CHARS + 500) + "\n❯ "; FakeHerdr herdr = new FakeHerdr().readText(longBlock); Rendezvous rendezvous = new Rendezvous();