diff options
| author | Claudomator Agent <agent@claudomator> | 2026-03-12 01:48:15 +0000 |
|---|---|---|
| committer | Claudomator Agent <agent@claudomator> | 2026-03-12 01:48:15 +0000 |
| commit | f28c22352aa1a8ede7552ee0277f7d60552d9094 (patch) | |
| tree | 02e9085809462e09b7d2a68ebb754e247eaecd22 /internal/api/server.go | |
| parent | 4f83d35fa47bc71b31e0f92a0927bea8910c01b6 (diff) | |
feat: add Resume support for CANCELLED, FAILED, and BUDGET_EXCEEDED tasks
Interrupted tasks (CANCELLED, FAILED, BUDGET_EXCEEDED) now support session
resume in addition to restart. Both buttons are shown on the task card.
- executor: extend resumablePoolStates to include CANCELLED, FAILED, BUDGET_EXCEEDED
- api: extend handleResumeTimedOutTask to accept all resumable states with
state-specific resume messages; replace hard-coded TIMED_OUT check with a
resumableStates map
- web: add RESUME_STATES set; render Resume + Restart buttons for interrupted
states; TIMED_OUT keeps Resume only
- tests: 5 new Go tests (TestResumeInterrupted_*); updated task-actions.test.mjs
with 17 tests covering dual-button behaviour
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/server.go')
| -rw-r--r-- | internal/api/server.go | 36 |
1 files changed, 33 insertions, 3 deletions
diff --git a/internal/api/server.go b/internal/api/server.go index c545253..df35536 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -24,6 +24,7 @@ type questionStore interface { GetLatestExecution(taskID string) (*storage.Execution, error) UpdateTaskQuestion(taskID, questionJSON string) error UpdateTaskState(id string, newState task.State) error + AppendTaskInteraction(taskID string, interaction task.Interaction) error } // Server provides the REST API and WebSocket endpoint for Claudomator. @@ -250,6 +251,25 @@ func (s *Server) handleAnswerQuestion(w http.ResponseWriter, r *http.Request) { return } + // Record the Q&A interaction before clearing the question. + if tk.QuestionJSON != "" { + var qData struct { + Text string `json:"text"` + Options []string `json:"options"` + } + if jsonErr := json.Unmarshal([]byte(tk.QuestionJSON), &qData); jsonErr == nil { + interaction := task.Interaction{ + QuestionText: qData.Text, + Options: qData.Options, + Answer: input.Answer, + AskedAt: tk.UpdatedAt, + } + if appendErr := s.questionStore.AppendTaskInteraction(taskID, interaction); appendErr != nil { + s.logger.Error("failed to append interaction", "taskID", taskID, "error", appendErr) + } + } + } + // Clear the question and transition to QUEUED. if err := s.questionStore.UpdateTaskQuestion(taskID, ""); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to clear question"}) @@ -277,6 +297,14 @@ func (s *Server) handleAnswerQuestion(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"message": "task queued for resume", "task_id": taskID}) } +// resumableStates are the task states from which a session-based resume is valid. +var resumableStates = map[task.State]string{ + task.StateTimedOut: "Your previous execution timed out. Please continue where you left off and complete the task.", + task.StateCancelled: "Your previous execution was cancelled. Please continue where you left off and complete the task.", + task.StateFailed: "Your previous execution failed. Please review what happened and continue from where you left off.", + task.StateBudgetExceeded: "Your previous execution exceeded its budget. Please continue where you left off and complete the task.", +} + func (s *Server) handleResumeTimedOutTask(w http.ResponseWriter, r *http.Request) { taskID := r.PathValue("id") @@ -285,8 +313,10 @@ func (s *Server) handleResumeTimedOutTask(w http.ResponseWriter, r *http.Request writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"}) return } - if tk.State != task.StateTimedOut { - writeJSON(w, http.StatusConflict, map[string]string{"error": "task is not timed out"}) + + resumeMsg, resumable := resumableStates[tk.State] + if !resumable { + writeJSON(w, http.StatusConflict, map[string]string{"error": "task is not in a resumable state"}) return } @@ -302,7 +332,7 @@ func (s *Server) handleResumeTimedOutTask(w http.ResponseWriter, r *http.Request ID: uuid.New().String(), TaskID: taskID, ResumeSessionID: latest.SessionID, - ResumeAnswer: "Your previous execution timed out. Please continue where you left off and complete the task.", + ResumeAnswer: resumeMsg, } if err := s.pool.SubmitResume(context.Background(), tk, resumeExec); err != nil { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": err.Error()}) |
