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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user