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
+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)
}
}