Confirm every write Orchestra sends, and count none of them as progress
F20. Only the launch confirmed its submit. A decision notice at a turn boundary, and /clear or @HANDOFF.md during a context reset, were fire-and-forget through the same transport that loses an Enter often enough that the launch needed three resubmits. A lost Enter on the context-reset path is the worst of them: it strands the session mid-rollover and nothing retries it. LaunchConfirmer is therefore InputConfirmer, ConfirmLaunch is ConfirmInput, and sendPrompt and sendLine both go through it. Orchestra does not try to guarantee delivery of input it did not originate. But it must never read that input as work, which is the F16 half. Burn-in run 3 stalled with an unexplained "go ahead and implement it" in the editor, and the renewal check hashed the whole capture, so those keystrokes read as progress and the lease kept renewing around an idle agent. PaneProgress drops input lines from the capture, which the -J join makes exact: a wrapped input block is one line beginning with the prompt marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+46
-12
@@ -172,9 +172,9 @@ func TestInputStateReadsTheEditorOwningTheCursor(t *testing.T) {
|
||||
|
||||
// Run 3 proved one Enter is not enough. A launch still owning the cursor must
|
||||
// be resubmitted, and the evidence must say how many submits it took.
|
||||
func TestConfirmLaunchResubmitsUntilTheEditorClears(t *testing.T) {
|
||||
func TestConfirmInputResubmitsUntilTheEditorClears(t *testing.T) {
|
||||
b, keys := submitTmux(t, 2)
|
||||
evidence, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
evidence, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
if err != nil {
|
||||
t.Fatalf("confirm: %v", err)
|
||||
}
|
||||
@@ -188,11 +188,11 @@ func TestConfirmLaunchResubmitsUntilTheEditorClears(t *testing.T) {
|
||||
|
||||
// The intervention is bounded independently of the observation deadline: a TUI
|
||||
// that already accepted the prompt must not receive a fortieth Enter.
|
||||
func TestConfirmLaunchBoundsResubmission(t *testing.T) {
|
||||
func TestConfirmInputBoundsResubmission(t *testing.T) {
|
||||
b, keys := submitTmux(t, 99)
|
||||
// Generous deadline on purpose: the cap must be what stops the resends.
|
||||
b.LaunchConfirmTimeout = 3 * time.Second
|
||||
if _, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference); !errors.Is(err, ErrPromptNotSubmitted) {
|
||||
if _, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference); !errors.Is(err, ErrPromptNotSubmitted) {
|
||||
t.Fatalf("err=%v, want ErrPromptNotSubmitted", err)
|
||||
}
|
||||
if got := enterCount(t, keys); got != launchResubmitLimit {
|
||||
@@ -202,10 +202,10 @@ func TestConfirmLaunchBoundsResubmission(t *testing.T) {
|
||||
|
||||
// Queued is submitted. Treating it as a stalled editor would kill a pane whose
|
||||
// harness had already accepted the instruction.
|
||||
func TestConfirmLaunchAcceptsQueuedInput(t *testing.T) {
|
||||
func TestConfirmInputAcceptsQueuedInput(t *testing.T) {
|
||||
pane := "\u2500\u2500\u2500\u2500\n\u276f Press up to edit queued messages\n"
|
||||
b := paneTmux(t, pane, 1)
|
||||
evidence, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
evidence, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
if err != nil {
|
||||
t.Fatalf("confirm: %v", err)
|
||||
}
|
||||
@@ -354,9 +354,9 @@ exit 0
|
||||
return &TmuxBackend{Binary: bin, LaunchConfirmTimeout: 2 * time.Second, LaunchConfirmPoll: 5 * time.Millisecond}
|
||||
}
|
||||
|
||||
func TestConfirmLaunchAcceptsObservedActivity(t *testing.T) {
|
||||
func TestConfirmInputAcceptsObservedActivity(t *testing.T) {
|
||||
b := fakeTmux(t, 1, false)
|
||||
evidence, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
evidence, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
if err != nil {
|
||||
t.Fatalf("confirm: %v", err)
|
||||
}
|
||||
@@ -368,10 +368,10 @@ func TestConfirmLaunchAcceptsObservedActivity(t *testing.T) {
|
||||
// Prompt returning nil is not proof. An editor that still holds the text at
|
||||
// the deadline is a launch failure, and it must be the one class that releases
|
||||
// the lease instead of holding it as uncertain.
|
||||
func TestConfirmLaunchFailsWhileTheEditorStillHoldsThePrompt(t *testing.T) {
|
||||
func TestConfirmInputFailsWhileTheEditorStillHoldsThePrompt(t *testing.T) {
|
||||
b := fakeTmux(t, 1000, false)
|
||||
b.LaunchConfirmTimeout = 60 * time.Millisecond
|
||||
_, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
_, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference)
|
||||
if !errors.Is(err, ErrPromptNotSubmitted) {
|
||||
t.Fatalf("err=%v, want ErrPromptNotSubmitted", err)
|
||||
}
|
||||
@@ -381,9 +381,43 @@ func TestConfirmLaunchFailsWhileTheEditorStillHoldsThePrompt(t *testing.T) {
|
||||
}
|
||||
|
||||
// A confirmation that cannot read the pane must not become an acknowledgement.
|
||||
func TestConfirmLaunchSurfacesTransportErrors(t *testing.T) {
|
||||
func TestConfirmInputSurfacesTransportErrors(t *testing.T) {
|
||||
b := fakeTmux(t, 0, true)
|
||||
if _, err := b.ConfirmLaunch(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference); err == nil {
|
||||
if _, err := b.ConfirmInput(context.Background(), Session{PaneID: "s:1.0"}, LaunchReference); err == nil {
|
||||
t.Fatal("a failed pane capture must not confirm a launch")
|
||||
}
|
||||
}
|
||||
|
||||
// F16 and F20 are separate concerns and this is the line between them:
|
||||
// Orchestra confirms delivery of what it originates, but nothing typed at a
|
||||
// pane counts as the agent doing work. A capture whose only difference is the
|
||||
// input line must produce the same progress digest.
|
||||
func TestPaneProgressIgnoresInputLines(t *testing.T) {
|
||||
body := "────\n ran the checks, nothing to change\n────\n"
|
||||
idle := paneTmux(t, body+"❯ \n", 3)
|
||||
typed := paneTmux(t, body+"❯ go ahead and implement it\n", 3)
|
||||
a, err := idle.PaneProgress(context.Background(), Session{PaneID: "s:1.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := typed.PaneProgress(context.Background(), Session{PaneID: "s:1.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a != b {
|
||||
t.Fatalf("typing changed the progress digest:\n%q\n%q", a, b)
|
||||
}
|
||||
if !strings.Contains(a, "ran the checks") {
|
||||
t.Fatalf("progress digest dropped harness output: %q", a)
|
||||
}
|
||||
|
||||
// Harness output still moves it.
|
||||
worked := paneTmux(t, "────\n edited scripts/orchestra_e2e_healthcheck.sh\n────\n❯ \n", 3)
|
||||
c, err := worked.PaneProgress(context.Background(), Session{PaneID: "s:1.0"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c == a {
|
||||
t.Fatal("real harness output left the progress digest unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user