CB-103: injector submits the prompt (Enter as a separate keystroke)
A delegated message was delivered into the worker's input box but never submitted, so no task was ever processed: bridge_send blocked until timeout while the worker sat idle with the prompt typed but not entered. herdr's agent.send delivers text as a bracketed paste; a trailing carriage return in that same call is swallowed as literal newline content, not Enter. AgentControl.send now emits two keystroke events — the payload, then a standalone "\r" — so the worker actually submits and runs the task. Verified live (Claude Code v2.1.210, off-sub worker): full hands-off bridge_send -> auto-submit -> worker computes -> bridge_reply round trip returns the reply in ~41s. 62 tests green.
This commit is contained in:
@@ -18,6 +18,17 @@ import java.util.Map;
|
||||
*/
|
||||
public final class AgentControl {
|
||||
|
||||
/**
|
||||
* The keystroke that submits a prompt in the Claude Code TUI: a carriage return (Enter).
|
||||
* It must be delivered as its <em>own</em> {@code agent.send} call — herdr delivers a message's
|
||||
* text as a bracketed paste, and a {@code "\r"} appended to that same text is swallowed as
|
||||
* literal newline content, not a submit. Sent as a separate keystroke event it lands outside
|
||||
* the paste and submits. (A bare {@code "\n"} inserts a newline either way.) Verified live
|
||||
* against Claude Code v2.1.210: an injected task stayed unsubmitted with {@code "text\r"} in
|
||||
* one call, and submitted the instant a standalone {@code "\r"} was sent.
|
||||
*/
|
||||
static final String SUBMIT_KEY = "\r";
|
||||
|
||||
private final HerdrClient herdr;
|
||||
|
||||
public AgentControl(HerdrClient herdr) {
|
||||
@@ -53,9 +64,15 @@ public final class AgentControl {
|
||||
return Agent.from(result.get("agent"));
|
||||
}
|
||||
|
||||
/** Deliver {@code text} to an agent (its next prompt input). */
|
||||
/**
|
||||
* Deliver {@code text} to an agent as its next prompt <em>and submit it</em> — two keystroke
|
||||
* events: the message (a bracketed paste, so any embedded newlines are preserved verbatim),
|
||||
* then a standalone {@link #SUBMIT_KEY} (Enter) that actually submits it. Without the second
|
||||
* event the text just sits in the worker's input box, never processed (see {@link #SUBMIT_KEY}).
|
||||
*/
|
||||
public void send(String target, String text) {
|
||||
herdr.call("agent.send", Map.of("target", target, "text", text));
|
||||
herdr.call("agent.send", Map.of("target", target, "text", SUBMIT_KEY));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package dev.ltms.bridged.herdr;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/** Unit-level behaviour of {@link AgentControl} over a fake herdr. */
|
||||
class AgentControlTest {
|
||||
|
||||
/** The {@code text} of every agent.send, in call order. */
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> sendTexts(FakeHerdr herdr) {
|
||||
return herdr.calls.stream()
|
||||
.filter(c -> c.method().equals("agent.send"))
|
||||
.map(c -> ((Map<String, Object>) c.params()).get("text").toString())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendDeliversThePayloadThenAStandaloneSubmitKey() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
new AgentControl(herdr).send("term_x", "do the thing");
|
||||
|
||||
// The Enter must be its own event — appended to the paste it would be swallowed as text.
|
||||
assertEquals(List.of("do the thing", "\r"), sendTexts(herdr),
|
||||
"payload paste first, then a separate carriage-return keystroke to submit it");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendPreservesEmbeddedNewlinesAndSubmitsOnlyOnce() {
|
||||
FakeHerdr herdr = new FakeHerdr();
|
||||
new AgentControl(herdr).send("term_x", "line1\nline2");
|
||||
|
||||
assertEquals(List.of("line1\nline2", "\r"), sendTexts(herdr),
|
||||
"multiline content is delivered verbatim; a single trailing Enter submits it");
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,18 @@ class InjectorTest {
|
||||
private final FakeHerdr herdr = new FakeHerdr();
|
||||
private final Injector injector = new Injector(new AgentControl(herdr));
|
||||
|
||||
/** Text of every agent.send, in order. */
|
||||
/**
|
||||
* The logical messages delivered, in order. AgentControl.send emits each delivery as two
|
||||
* agent.send calls — the payload, then a standalone Enter keystroke ({@code "\r"}) to submit
|
||||
* it; these tests assert delivery ordering/gating, not the submit event, so drop the bare
|
||||
* carriage returns.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> sent() {
|
||||
return herdr.calls.stream()
|
||||
.filter(c -> c.method().equals("agent.send"))
|
||||
.map(c -> ((Map<String, Object>) c.params()).get("text").toString())
|
||||
.filter(t -> !t.equals("\r"))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -170,7 +176,9 @@ class InjectorTest {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> p = (Map<String, Object>) c.params();
|
||||
return p.get("text").toString();
|
||||
}).toList());
|
||||
})
|
||||
.filter(t -> !t.equals("\r")) // drop the standalone submit keystroke
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user