diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 21:45:11 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-08 21:45:11 +0000 |
| commit | a6926b80283bbe855bb617be0dbdbaa9968f62f8 (patch) | |
| tree | 4b1e9b1d8c21d039e2743214d9a79981e29d5b16 /internal/api | |
| parent | e6e1e7cd6d79eb969345e738f2554108681ade95 (diff) | |
fix: retry limits apply only to automatic retries, not manual runs
Remove the MaxAttempts check from POST /api/tasks/{id}/run. A user
explicitly triggering a run is a manual action and should not be gated
by the retry limit. Retry limits will be enforced in the (future)
automatic retry path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/server.go | 15 | ||||
| -rw-r--r-- | internal/api/server_test.go | 28 |
2 files changed, 12 insertions, 31 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index 34e1872..11c9c15 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -451,21 +451,6 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) { return } - // Enforce retry limit for non-initial runs (PENDING is the initial state). - if t.State != task.StatePending { - execs, err := s.store.ListExecutions(id) - if err != nil { - writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to count executions"}) - return - } - if t.Retry.MaxAttempts > 0 && len(execs) >= t.Retry.MaxAttempts { - writeJSON(w, http.StatusConflict, map[string]string{ - "error": fmt.Sprintf("retry limit reached (%d/%d attempts used)", len(execs), t.Retry.MaxAttempts), - }) - return - } - } - if err := s.store.UpdateTaskState(id, task.StateQueued); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return diff --git a/internal/api/server_test.go b/internal/api/server_test.go index cd415ae..8484e02 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -604,14 +604,14 @@ func TestResumeTimedOut_Success_Returns202(t *testing.T) { } } -func TestRunTask_RetryLimitReached_Returns409(t *testing.T) { +// TestRunTask_ManualRunIgnoresRetryLimit verifies that a manual POST /run always +// succeeds regardless of MaxAttempts — retry limits only gate automatic retries. +func TestRunTask_ManualRunIgnoresRetryLimit(t *testing.T) { srv, store := testServer(t) - // Task with MaxAttempts: 1 — only 1 attempt allowed. Create directly as FAILED - // so state is consistent without going through transition sequence. tk := &task.Task{ - ID: "retry-limit-1", + ID: "retry-limit-manual", Name: "Retry Limit Task", - Agent: task.AgentConfig{Instructions: "do something"}, + Agent: task.AgentConfig{Instructions: "do something"}, Priority: task.PriorityNormal, Retry: task.RetryConfig{MaxAttempts: 1, Backoff: "linear"}, Tags: []string{}, @@ -621,10 +621,10 @@ func TestRunTask_RetryLimitReached_Returns409(t *testing.T) { if err := store.CreateTask(tk); err != nil { t.Fatal(err) } - // Record one execution — the first attempt already used. + // Record one execution — MaxAttempts already exhausted. exec := &storage.Execution{ - ID: "exec-retry-1", - TaskID: "retry-limit-1", + ID: "exec-retry-manual", + TaskID: "retry-limit-manual", StartTime: time.Now(), Status: "FAILED", } @@ -632,17 +632,13 @@ func TestRunTask_RetryLimitReached_Returns409(t *testing.T) { t.Fatal(err) } - req := httptest.NewRequest("POST", "/api/tasks/retry-limit-1/run", nil) + req := httptest.NewRequest("POST", "/api/tasks/retry-limit-manual/run", nil) w := httptest.NewRecorder() srv.Handler().ServeHTTP(w, req) - if w.Code != http.StatusConflict { - t.Errorf("status: want 409, got %d; body: %s", w.Code, w.Body.String()) - } - var body map[string]string - json.NewDecoder(w.Body).Decode(&body) - if !strings.Contains(body["error"], "retry limit") { - t.Errorf("error body should mention retry limit, got %q", body["error"]) + // Manual run must succeed (202) even though MaxAttempts is exhausted. + if w.Code != http.StatusAccepted { + t.Errorf("status: want 202, got %d; body: %s", w.Code, w.Body.String()) } } |
