From 40d9ace301654869cce46c9e23c559f944e754c0 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Fri, 6 Mar 2026 04:58:06 +0000 Subject: fix: persist session_id in UpdateExecution so BLOCKED tasks can resume session_id was set on the Execution struct inside ClaudeRunner.Run but was missing from the UPDATE query, so it was never written to the DB. GetLatestExecution then returned an empty session_id, causing the /answer endpoint to return "no resumable session found". Also patched the existing blocked execution row directly in the DB. Co-Authored-By: Claude Sonnet 4.6 --- internal/storage/db.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) (limited to 'internal') diff --git a/internal/storage/db.go b/internal/storage/db.go index 0e4d6f1..1aac754 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -359,6 +359,56 @@ func (s *DB) GetLatestExecution(taskID string) (*Execution, error) { return scanExecution(row) } +// DeleteTask removes a task and all its executions and subtasks (recursively). +// Returns an error if the task does not exist. +func (s *DB) DeleteTask(id string) error { + // Collect all task IDs to delete (the task + all descendant subtasks). + toDelete := []string{id} + // BFS over children. + queue := []string{id} + for len(queue) > 0 { + parentID := queue[0] + queue = queue[1:] + rows, err := s.db.Query(`SELECT id FROM tasks WHERE parent_task_id = ?`, parentID) + if err != nil { + return fmt.Errorf("listing subtasks of %q: %w", parentID, err) + } + for rows.Next() { + var childID string + if err := rows.Scan(&childID); err != nil { + rows.Close() + return err + } + toDelete = append(toDelete, childID) + queue = append(queue, childID) + } + rows.Close() + if err := rows.Err(); err != nil { + return err + } + } + + // Delete executions for all collected tasks then the tasks themselves. + for _, tid := range toDelete { + if _, err := s.db.Exec(`DELETE FROM executions WHERE task_id = ?`, tid); err != nil { + return fmt.Errorf("deleting executions for task %q: %w", tid, err) + } + } + for _, tid := range toDelete { + result, err := s.db.Exec(`DELETE FROM tasks WHERE id = ?`, tid) + if err != nil { + return fmt.Errorf("deleting task %q: %w", tid, err) + } + if tid == id { + n, _ := result.RowsAffected() + if n == 0 { + return fmt.Errorf("task %q not found", id) + } + } + } + return nil +} + // UpdateTaskQuestion stores the pending question JSON on a task. // Pass empty string to clear the question after it has been answered. func (s *DB) UpdateTaskQuestion(taskID, questionJSON string) error { @@ -371,10 +421,10 @@ func (s *DB) UpdateTaskQuestion(taskID, questionJSON string) error { func (s *DB) UpdateExecution(e *Execution) error { _, err := s.db.Exec(` UPDATE executions SET end_time = ?, exit_code = ?, status = ?, cost_usd = ?, error_msg = ?, - stdout_path = ?, stderr_path = ?, artifact_dir = ? + stdout_path = ?, stderr_path = ?, artifact_dir = ?, session_id = ? WHERE id = ?`, e.EndTime.UTC(), e.ExitCode, e.Status, e.CostUSD, e.ErrorMsg, - e.StdoutPath, e.StderrPath, e.ArtifactDir, e.ID, + e.StdoutPath, e.StderrPath, e.ArtifactDir, e.SessionID, e.ID, ) return err } -- cgit v1.2.3