Compare commits

...

1 Commits

Author SHA1 Message Date
Dai Ha ce6e6d11c6 fleetd #782: PromptBox ignores non-breaking space padding and bounds the hold streak
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 55s
CI / build (pull_request) Failing after 2m1s
boxContent now treats Character.isSpaceChar(c) the same as isWhitespace(c), so a
no-break space (U+00A0) padding the box no longer reads as the operator's text.

clearToSubmit now gives up after HOLD_GIVE_UP_STREAK consecutive holds on one
target and lets the delivery through, instead of holding a channel shut forever
when the box never reads EMPTY.
2026-10-05 20:03:52 +02:00
2 changed files with 77 additions and 8 deletions
@@ -18,11 +18,13 @@ import java.util.concurrent.ConcurrentHashMap;
*
* <p>Only a box that is positively empty clears the gate. A box with content, a pane this cannot
* recognise, and a failed read all hold the delivery, because a held delivery is recoverable and a
* submitted half-line is not. Every caller must therefore be a path that retries.
* submitted half-line is not. A hold is bounded, though: after {@link #HOLD_GIVE_UP_STREAK}
* consecutive holds on one target, the gate gives up and lets the delivery through anyway, because a
* channel that never delivers again is worse than one clobbered line.
*
* <p>A pane that holds for {@link #HOLD_WARN_STREAK} consecutive checks gets one warning, so a box
* that never clears is visible instead of silent. The warning repeats only after the box has cleared
* again.
* again, and giving up logs its own warning the same way.
*/
public final class PromptBox {
@@ -39,6 +41,9 @@ public final class PromptBox {
/** Consecutive holds for one target before one warning is logged. */
static final int HOLD_WARN_STREAK = 20;
/** Consecutive holds for one target before the gate stops holding and lets the delivery through. */
static final int HOLD_GIVE_UP_STREAK = 1200;
/**
* Input box markers, each matched only as a line's first characters: the caret the current TUI
* draws, and the bordered box an older one drew. A marker further along a line is transcript text,
@@ -70,7 +75,9 @@ public final class PromptBox {
/**
* Whether {@code target}'s input box is empty, so a delivery would submit only its own text.
* {@code false} means hold and come back; it never means the delivery failed.
* {@code false} means hold and come back; it never means the delivery failed. After {@link
* #HOLD_GIVE_UP_STREAK} consecutive holds on the same target, this returns {@code true} instead,
* so the delivery goes through rather than holding the channel shut forever.
*/
public boolean clearToSubmit(String target) {
Reading reading = inspect(target);
@@ -79,6 +86,13 @@ public final class PromptBox {
return true;
}
int streak = holdStreaks.merge(target, 1, Integer::sum);
if (streak >= HOLD_GIVE_UP_STREAK) {
holdStreaks.remove(target);
log.warn("prompt box of {} never cleared after {} holds ({}, {} character(s) in the box)"
+ " — giving up and sending this delivery anyway, which may submit whatever is in the box",
target, streak, reading.state(), reading.characters());
return true;
}
if (streak == HOLD_WARN_STREAK) {
log.warn("prompt box of {} has held a delivery {} times in a row ({}, {} character(s) in the box)"
+ " — nothing is lost, delivery resumes once the box is empty",
@@ -114,10 +128,9 @@ public final class PromptBox {
* an earlier turn's marker survives; treating that as a live turn would make {@link State#EMPTY}
* unreachable and hold every delivery forever.
*
* <p>Whitespace, a trailing box border and a cursor block count as nothing. Any other character
* counts as the operator's unsubmitted text, including a placeholder hint a future TUI might draw
* there; that direction holds a delivery it could have sent, which {@link #HOLD_WARN_STREAK} makes
* visible.
* <p>Whitespace — including non-breaking variants such as U+00A0 — a trailing box border and a
* cursor block all count as nothing. Any other character counts as the operator's unsubmitted
* text, including a placeholder hint a TUI might draw there.
*/
static Reading classify(String pane) {
if (pane == null || pane.isBlank()) return new Reading(State.UNREADABLE, 0);
@@ -161,7 +174,7 @@ public final class PromptBox {
if (line.endsWith("│")) line = line.substring(0, line.length() - 1);
StringBuilder content = new StringBuilder();
for (char c : line.toCharArray()) {
if (Character.isWhitespace(c) || CURSOR_GLYPHS.indexOf(c) >= 0) continue;
if (Character.isWhitespace(c) || Character.isSpaceChar(c) || CURSOR_GLYPHS.indexOf(c) >= 0) continue;
content.append(c);
}
return content.toString();
@@ -90,6 +90,25 @@ class PromptBoxTest {
"that marker survives in scrollback, and holding on it would hold every delivery forever");
}
@Test
void aNoBreakSpacePaddedBoxIsEmpty() {
assertEquals(new PromptBox.Reading(PromptBox.State.EMPTY, 0),
PromptBox.classify("❯     "));
}
@Test
void anOrdinarySpacePaddedBoxIsStillEmpty() {
assertEquals(new PromptBox.Reading(PromptBox.State.EMPTY, 0), PromptBox.classify("❯ "));
}
@Test
void aNoBreakSpaceLeadInDoesNotCountTowardTheDraftsCharacters() {
PromptBox.Reading reading = PromptBox.classify("❯ hello");
assertEquals(PromptBox.State.DRAFT, reading.state());
assertEquals("hello".length(), reading.characters(),
"the no-break space before the typed text does not count, only the text itself");
}
// --- the gate ------------------------------------------------------------
@Test
@@ -129,4 +148,41 @@ class PromptBoxTest {
herdr.detectionText(EMPTY);
assertTrue(box.clearToSubmit("term_a"));
}
@Test
void aBoxThatNeverClearsIsLetThroughOnceTheGiveUpStreakIsReached() {
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
PromptBox box = new PromptBox(new AgentControl(herdr));
for (int i = 1; i < PromptBox.HOLD_GIVE_UP_STREAK; i++) {
assertFalse(box.clearToSubmit("term_a"), "still holding before the give-up streak is reached");
}
assertTrue(box.clearToSubmit("term_a"), "the gate gives up and lets the delivery through");
}
@Test
void givingUpResetsTheStreakSoTheNextHoldStartsOver() {
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
PromptBox box = new PromptBox(new AgentControl(herdr));
for (int i = 0; i < PromptBox.HOLD_GIVE_UP_STREAK; i++) {
box.clearToSubmit("term_a");
}
assertFalse(box.clearToSubmit("term_a"), "the streak started over, so one more hold is not another give-up");
}
@Test
void theStreakResetsWhenTheBoxEmptiesBeforeTheGiveUpStreak() {
FakeHerdr herdr = new FakeHerdr().detectionText(DRAFTED);
PromptBox box = new PromptBox(new AgentControl(herdr));
for (int i = 0; i < PromptBox.HOLD_GIVE_UP_STREAK - 1; i++) {
box.clearToSubmit("term_a");
}
herdr.detectionText(EMPTY);
assertTrue(box.clearToSubmit("term_a"));
herdr.detectionText(DRAFTED);
assertFalse(box.clearToSubmit("term_a"), "the streak started over, so one more hold is not a give-up");
}
}