Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1933,7 +1933,16 @@ func (e *Executor) executeTask(ctx context.Context, task *db.Task) {

// Update final status and trigger hooks
// Respect status set by hooks - don't override blocked with done
if result.Interrupted {
if result.Requeued {
// The poller exited because the task was re-queued (e.g. the user flipped a
// blocked task back to "In Progress"), NOT because it was cancelled. Leave the
// task in 'queued' and let the worker pick it up for a fresh run — do NOT write
// backlog. Releasing this run's slot (via the deferred runningTasks cleanup)
// plus the worker's poll is enough; the fresh run's KillAllWindowsByNameAllSessions
// tears down the stale window, so no duplicate session lingers.
e.logLine(task.ID, "system", "Task re-queued - starting a fresh run")
e.TriggerProcessing()
} else if result.Interrupted {
// Explicitly set to backlog - don't assume Interrupt() already did it,
// as the interruption may have come from pollTmuxSession detecting a
// stale status or context cancellation.
Expand Down Expand Up @@ -2414,6 +2423,7 @@ type execResult struct {
Success bool
NeedsInput bool
Interrupted bool
Requeued bool
Message string
}

Expand Down Expand Up @@ -4094,9 +4104,12 @@ func (e *Executor) pollTmuxSession(ctx context.Context, taskID int64, sessionNam
return execResult{Success: true}
}
if task.Status == db.StatusQueued {
// Task was re-queued (e.g. retry) - stop polling so the
// worker can pick it up fresh without a duplicate session
return execResult{Interrupted: true}
// Task was re-queued (e.g. retry, or the user manually flipped a
// blocked task back to "In Progress") - stop polling so the worker
// can pick it up fresh without a duplicate session. Report this as
// Requeued, NOT Interrupted: an interrupt is finalized to backlog
// (a cancellation), which would immediately undo the requeue.
return execResult{Requeued: true}
}
}

Expand Down
47 changes: 47 additions & 0 deletions internal/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1936,3 +1936,50 @@ func TestBuildPromptUsesFileKindInstructions(t *testing.T) {
t.Errorf("prompt should include the file kind's instructions; got:\n%s", prompt)
}
}

// TestPollTmuxSessionRequeuesOnQueuedStatus verifies that when a task's status
// flips to "queued" while its poller is still alive (e.g. the user manually
// changes a blocked task back to "In Progress"), pollTmuxSession reports the
// exit as Requeued — NOT Interrupted. Reporting Interrupted would cause
// executeTask to finalize the task to backlog, immediately undoing the requeue
// (github.com/bborn/taskyou/issues/674).
func TestPollTmuxSessionRequeuesOnQueuedStatus(t *testing.T) {
tmpFile, err := os.CreateTemp("", "test-*.db")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpFile.Name())
tmpFile.Close()

database, err := db.Open(tmpFile.Name())
if err != nil {
t.Fatal(err)
}
defer database.Close()

if err := database.CreateProject(&db.Project{Name: "test", Path: "/tmp/test"}); err != nil {
t.Fatal(err)
}

exec := New(database, &config.Config{})

// Task is queued (as showChangeStatus writes when mapping "In Progress").
task := &db.Task{Title: "Test task", Status: db.StatusQueued, Project: "test"}
if err := database.CreateTask(task); err != nil {
t.Fatal(err)
}

// The DB-status check is the first thing pollTmuxSession does each tick, so it
// returns before touching tmux. Give it enough headroom for one tick.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result := exec.pollTmuxSession(ctx, task.ID, "nonexistent-session")

if !result.Requeued {
t.Errorf("expected Requeued=true for a re-queued task, got %+v", result)
}
if result.Interrupted {
t.Error("expected Interrupted=false for a re-queued task; a re-queue must not be finalized to backlog")
}
}
1 change: 1 addition & 0 deletions internal/executor/task_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type ExecResult struct {
Success bool // Task completed successfully
NeedsInput bool // Task is waiting for user input
Interrupted bool // Task was interrupted by user
Requeued bool // Task was re-queued (e.g. manual "In Progress") and should resume via a fresh run, not be treated as a cancellation
Message string // Status message or error
}

Expand Down