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