Give the operator a way to resubmit, and baseline progress at launch

Two gaps F33 left behind.

F34: the renewal gate exempts a lease with no baseline, so a pane that opened
and never started got a full free renewal period. That is the exact case the
gate exists to catch, and it happened live at 01:33:37: the stuck run 5 lease
renewed to 22:03 on a pane that had not moved since 01:13. Baseline the
progress hash at launch, where the pane is already being read.

F35: nothing could re-poke a live pane. Orchestra can put text in an editor and
be wrong about whether it landed, and the only recovery was to destroy the
lease and wait out expiry, roughly an hour. The new "resubmit" action presses
Enter on text Orchestra itself submitted. It changes no lifecycle state so it
appends no event, and it is fenced like an approval: a live worker-owned pane
at the capture revision the operator was looking at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xsXyr5J1RACo71YeKG3Pu
This commit is contained in:
2026-08-28 01:34:26 +04:00
parent 48326d4301
commit 92f32d6fea
4 changed files with 107 additions and 2 deletions
+26
View File
@@ -475,6 +475,19 @@ func (w *worker) start(ctx context.Context, t domain.Task, ref string) error {
}
log.Printf("launch %s confirmed: %s", t.ID, evidence)
}
// Baseline the progress check at launch, not at the first renewal. The
// renewal gate exempts a lease with no baseline, which handed a pane that
// opened and never started a full free renewal period (F34) — the exact
// case the gate exists to catch. Capturing here costs one pane read and
// makes the first renewal a real comparison.
if l, ok := w.leases[t.ID]; ok && l.ProgressSHA == "" {
if text, progressErr := w.paneProgress(ctx, herdr.CLIAdapter{Backend: backend, Harness: w.harness}, s); progressErr == nil {
l.ProgressSHA = domain.Hash([]byte(text))
w.leases[t.ID] = l
} else {
w.recordError(fmt.Errorf("baseline launch progress %s: %w", t.ID, progressErr))
}
}
if l, ok := w.leases[t.ID]; ok {
if err := w.api.Start(ctx, t.ID, l.Epoch, l.Version, w.sessionEvidence(ctx, t.ID, s)); err != nil {
return fmt.Errorf("ack start: %w", err)
@@ -1093,6 +1106,19 @@ func (w *worker) runCommands(ctx context.Context) {
_ = w.api.ResolveCommand(ctx, command.ID, "stale", "capture revision changed")
continue
}
if command.Kind == "resubmit" {
// Not an approval: the pane is holding input Orchestra already
// submitted and believes it delivered. Press Enter and say so.
if err := w.executionBackend().SendKeys(ctx, session, []string{"Enter"}); err != nil {
_ = w.api.ResolveCommand(ctx, command.ID, "rejected", "resubmit not delivered: "+err.Error())
continue
}
log.Printf("resubmit %s: Enter resent to %s at capture revision %d", command.TaskID, session.PaneID, command.CaptureRevision)
if err := w.api.ResolveCommand(ctx, command.ID, "acknowledged", ""); err != nil {
log.Printf("ack command %s: %v", command.ID, err)
}
continue
}
input, ok := approvalResponse(text, command.Kind)
if !ok {
_ = w.api.ResolveCommand(ctx, command.ID, "rejected", "prompt does not expose an executable approval control")
+5 -1
View File
@@ -237,7 +237,11 @@ func (r *Registry) Queue(worker string, c Command) (Command, error) {
if _, ok := r.workers[worker]; !ok {
return Command{}, ErrUnknownWorker
}
if c.TaskID == "" || c.PaneID == "" || c.CaptureRevision == 0 || (c.Kind != "grant_approval" && c.Kind != "deny_approval") {
// "resubmit" presses Enter on input Orchestra already submitted and the
// harness never took (F33). It carries no decision, so it is not an
// approval, but it is fenced the same way: a live pane and the capture
// revision the operator was looking at.
if c.TaskID == "" || c.PaneID == "" || c.CaptureRevision == 0 || (c.Kind != "grant_approval" && c.Kind != "deny_approval" && c.Kind != "resubmit") {
return Command{}, errors.New("invalid control command")
}
c.ID = fmt.Sprintf("cmd-%x", sha256.Sum256([]byte(fmt.Sprintf("%s/%s/%s/%d/%d", worker, c.TaskID, c.Kind, c.CaptureRevision, time.Now().UnixNano()))))[:20]
+28 -1
View File
@@ -210,7 +210,7 @@ func capturePane(s Server, t domain.Task, c *Capture) string {
}
func actions(t domain.Task) []Action {
active := t.State == domain.StateLeased
return []Action{{ID: "handoff", Enabled: active, Reason: "requires a live leased session"}, {ID: "release", Enabled: active, Needs: []string{"reason or handoff_ref"}}, {ID: "block", Enabled: active, Needs: []string{"blocker"}}, {ID: "complete", Enabled: active, Needs: []string{"report_ref", "receipt"}}}
return []Action{{ID: "handoff", Enabled: active, Reason: "requires a live leased session"}, {ID: "resubmit", Enabled: active, Reason: "resends Enter to a pane still holding unsent input"}, {ID: "release", Enabled: active, Needs: []string{"reason or handoff_ref"}}, {ID: "block", Enabled: active, Needs: []string{"blocker"}}, {ID: "complete", Enabled: active, Needs: []string{"report_ref", "receipt"}}}
}
func (s Server) Overview(ctx context.Context) Overview {
// JSON null is not an empty collection to browser clients. In particular,
@@ -400,6 +400,33 @@ func (s Server) action(w http.ResponseWriter, r *http.Request, id, action string
d, _ := s.detail(r.Context(), id)
writeJSON(w, d)
return
case "resubmit":
// Orchestra can put text in an editor and be wrong about whether it
// landed (F33). Until this existed the only way to recover a pane
// holding unsent input was to destroy the lease and wait for expiry,
// which cost the better part of an hour and threw away the session.
//
// This presses Enter on text Orchestra itself submitted. It changes no
// lifecycle state, so it appends no event; the worker logs the
// keystroke and the capture revision fences it against a pane that has
// moved on since the operator looked.
c, err := s.capture(r.Context(), t)
if err != nil || c == nil {
http.Error(w, "current capture unavailable", 503)
return
}
pane := capturePane(s, t, c)
if pane == "" || c.Source != "worker" {
http.Error(w, "resubmit needs a live worker-owned pane", 409)
return
}
command, err := s.Workers.Queue(t.Lease.HarnessID, federation.Command{TaskID: id, Kind: "resubmit", PaneID: pane, CaptureRevision: c.Revision})
if err != nil {
http.Error(w, err.Error(), 409)
return
}
writeJSON(w, map[string]any{"command": command, "task": t})
return
case "handoff":
if s.Coordinator == nil {
http.Error(w, "live coordinator unavailable", 503)
+48
View File
@@ -187,3 +187,51 @@ func TestActionWithoutABodyDoesNotPanic(t *testing.T) {
t.Fatalf("block status=%d body=%s, want 409 blocker required", r.Code, r.Body.String())
}
}
// TestResubmitQueuesAKeystrokeWithoutTouchingLifecycle covers the gap F33 left
// behind: a pane holding unsent input had no recovery short of destroying the
// lease and waiting out expiry. Resubmit presses Enter on text Orchestra
// already submitted, so it queues a worker command and appends no event.
func TestResubmitQueuesAKeystrokeWithoutTouchingLifecycle(t *testing.T) {
s, err := store.Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
workers := &federation.Registry{}
if err := workers.Register(federation.Worker{ID: "wpc-claude", Token: "tok"}, ""); err != nil {
t.Fatal(err)
}
h := Server{Store: s, Workers: workers}.Handler()
req := httptest.NewRequest(http.MethodPost, "/v1/ui/tasks", bytes.NewBufferString(`{"source":"web","external_id":"stuck","project":"demo","title":"Stuck launch"}`))
r := httptest.NewRecorder()
h.ServeHTTP(r, req)
id := s.Tasks()[0].ID
if _, err := s.Lease(id, "wpc-claude", time.Hour); err != nil {
t.Fatal(err)
}
before := len(s.Events(0))
capture, err := workers.PutCapture("wpc-claude", federation.Capture{TaskID: id, PaneID: "wA:p1", Text: "> read .orchestra/launch.md now"})
if err != nil {
t.Fatal(err)
}
req = httptest.NewRequest(http.MethodPost, "/v1/ui/tasks/"+id+"/actions/resubmit", bytes.NewBufferString(`{}`))
r = httptest.NewRecorder()
h.ServeHTTP(r, req)
if r.Code != http.StatusOK {
t.Fatalf("resubmit status=%d body=%s", r.Code, r.Body.String())
}
pending, err := workers.Commands("wpc-claude")
if err != nil || len(pending) != 1 {
t.Fatalf("commands=%#v err=%v", pending, err)
}
if pending[0].Kind != "resubmit" || pending[0].PaneID != "wA:p1" || pending[0].CaptureRevision != capture.Revision {
t.Fatalf("queued %#v, want a resubmit bound to wA:p1 at revision %d", pending[0], capture.Revision)
}
if got := len(s.Events(0)); got != before {
t.Fatalf("events %d -> %d: a keystroke must not append a lifecycle event", before, got)
}
task, _ := s.Task(id)
if task.State != domain.StateLeased {
t.Fatalf("state=%q, want the lease untouched", task.State)
}
}