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:
2026-08-27 14:44:04 +04:00
parent 770cc6a74b
commit edbe98fc5e
5 changed files with 175 additions and 30 deletions
+47 -7
View File
@@ -370,10 +370,17 @@ func TestApprovalResponseOpenCodeAllowOnce(t *testing.T) {
func mustJSON(v any) []byte { b, _ := json.Marshal(v); return b }
type recordingBackend struct {
status string
capture string
calls []string
prompts []string
status string
capture string
progress string
calls []string
prompts []string
}
// PaneProgress makes this stub the kind of backend that can separate harness
// output from input, which is what the worker must prefer over a raw capture.
func (b *recordingBackend) PaneProgress(context.Context, herdr.Session) (string, error) {
return b.progress, nil
}
func (b *recordingBackend) Kind() string { return "recording" }
@@ -406,7 +413,7 @@ func TestRenewLeasesRequiresProgress(t *testing.T) {
rw.Write([]byte(`{}`))
}))
defer api.Close()
backend := &recordingBackend{status: "idle", capture: "same screen"}
backend := &recordingBackend{status: "idle", progress: "same screen"}
w := &worker{
api: federation.Client{BaseURL: api.URL, WorkerID: "h", Token: "t"},
backend: backend,
@@ -425,7 +432,7 @@ func TestRenewLeasesRequiresProgress(t *testing.T) {
if renewals != 1 {
t.Fatalf("busy agent renewals=%d, want 1", renewals)
}
backend.status, backend.capture = "idle", "new output"
backend.status, backend.progress = "idle", "new output"
l := w.leases["task"]
l.Until = time.Now()
w.leases["task"] = l
@@ -764,7 +771,7 @@ type launchBackend struct {
}
func (b *launchBackend) LaunchTransport(string) herdr.LaunchTransport { return b.transport }
func (b *launchBackend) ConfirmLaunch(context.Context, herdr.Session, string) (string, error) {
func (b *launchBackend) ConfirmInput(context.Context, herdr.Session, string) (string, error) {
if !b.confirmed {
return "", fmt.Errorf("%w: editor still holds the prompt", herdr.ErrPromptNotSubmitted)
}
@@ -915,3 +922,36 @@ func TestReconcileLeasesKeepsWorkerLocalObservations(t *testing.T) {
t.Fatalf("observations survived a new epoch: %+v", got)
}
}
// The live shape of the run-3 stall: someone typed into the pane, the capture
// changed, and nothing the agent did changed at all. Renewal must refuse.
func TestRenewLeasesIgnoresPaneInput(t *testing.T) {
renewals := 0
api := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
renewals++
rw.Write([]byte(`{}`))
}))
defer api.Close()
backend := &recordingBackend{status: "idle", capture: "screen\n\u276f ", progress: "screen"}
w := &worker{
api: federation.Client{BaseURL: api.URL, WorkerID: "h", Token: "t"},
backend: backend,
harness: "claude",
sessions: map[string]herdr.Session{"task": {PaneID: "pane"}},
leases: map[string]lease{"task": {Epoch: "e", Version: 1, Until: time.Now(), ProgressSHA: domain.Hash([]byte("screen"))}},
quarantined: map[string]bool{},
statePath: filepath.Join(t.TempDir(), "state.json"),
}
// Only the input line changes. The agent has done nothing.
backend.capture = "screen\n\u276f go ahead and implement it"
w.renewLeases(context.Background())
if renewals != 0 {
t.Fatalf("pane input renewed the lease %d times", renewals)
}
// Harness output moves the digest, and only then does the lease renew.
backend.progress = "screen\nedited the script"
w.renewLeases(context.Background())
if renewals != 1 {
t.Fatalf("renewals=%d, want 1 after real progress", renewals)
}
}