Give an operator one way to retry a terminal task
A task that reached the router's MaxAttempts was permanently terminal.
TaskReleased only ever increments Attempt, TaskCorrected could not touch it,
and no HTTP route emitted a correction at all. The only way to work an
exhausted issue again was to invent a second task for it, which defeats
(source, external_id) dedupe and abandons the task's own history.
POST /v1/tasks/{id}/retry, full-control surfaces only. It requires the task
to be failed, unleased, and failed with reason retry_limit: restoring a retry
budget is not an answer to a failure that was not the budget running out. The
effect is one TaskCorrected naming that failure, setting state queued and
attempt 0 and clearing next_retry_at, failure_class and last_error. Task id,
source pair, goal, acceptance, decisions, work phase and artifact refs all
stay, and the original failure events stay in the log.
operation_id is required and makes the call idempotent, so a repeated request
cannot reset an attempt that has since started running.
This is RetryTask, not a generic correction endpoint: arbitrary task mutation
over HTTP is a different and much larger authority. It also does not address
F9, which is an operator releasing a lease someone else owns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+21
-1
@@ -954,7 +954,7 @@ func main() {
|
||||
}
|
||||
var e domain.Event
|
||||
var err error
|
||||
actionTypes := map[string]string{"lease": "TaskLeased", "release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked", "attention": "TaskNeedsAttention"}
|
||||
actionTypes := map[string]string{"lease": "TaskLeased", "release": "TaskReleased", "complete": "TaskCompleted", "block": "TaskBlocked", "attention": "TaskNeedsAttention", "retry": "TaskCorrected"}
|
||||
if typ, known := actionTypes[action]; known {
|
||||
if err := authz.AuthorizeEvent(surface(r), typ); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
@@ -962,6 +962,26 @@ func main() {
|
||||
}
|
||||
}
|
||||
switch action {
|
||||
case "retry":
|
||||
// Narrow on purpose: RetryTask restores a retry budget on a
|
||||
// terminal task. There is no generic correction endpoint, because
|
||||
// one would be arbitrary task mutation over HTTP.
|
||||
var p struct {
|
||||
OperationID string `json:"operation_id"`
|
||||
}
|
||||
if r.Body == nil || json.NewDecoder(r.Body).Decode(&p) != nil || p.OperationID == "" {
|
||||
http.Error(w, "operation_id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
e, err = operations.RetryTask(s, surface(r), taskID, p.OperationID)
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotFound):
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
case errors.Is(err, operations.ErrNotRetryable), errors.Is(err, domain.ErrInvalid):
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
case "lease":
|
||||
var p struct {
|
||||
HarnessID string `json:"harness_id"`
|
||||
|
||||
Reference in New Issue
Block a user