diff --git a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts index 95bcb9d8e6..56d52f3ba0 100644 --- a/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts @@ -161,6 +161,11 @@ export function registerTaskExecutionHandlers( console.warn('[TASK_START] Found task:', task.specId, 'status:', task.status, 'reviewReason:', task.reviewReason, 'subtasks:', task.subtasks.length); + // Clear stale tracking state from any previous execution so that: + // - terminalEventSeen doesn't suppress future PROCESS_EXITED events + // - lastSequenceByTask doesn't drop events from the new process + taskStateManager.prepareForRestart(taskId); + // Immediately mark as started so the UI moves the card to In Progress. // Use XState actor state as source of truth (if actor exists), with task data as fallback. // - plan_review: User approved the plan, send PLAN_APPROVED to transition to coding @@ -315,6 +320,9 @@ export function registerTaskExecutionHandlers( task, project ); + + // Clear stale tracking state so a subsequent restart works correctly + taskStateManager.prepareForRestart(taskId); }); /** @@ -484,6 +492,9 @@ export function registerTaskExecutionHandlers( return { success: false, error: 'Failed to write QA fix request file' }; } + // Clear stale tracking state before starting new QA process + taskStateManager.prepareForRestart(taskId); + // Restart QA process - use worktree path if it exists, otherwise main project // The QA process needs to run where the implementation_plan.json with completed subtasks is const qaProjectPath = hasWorktree ? worktreePath : project.path; @@ -658,6 +669,8 @@ export function registerTaskExecutionHandlers( // Auto-start task when status changes to 'in_progress' and no process is running if (status === 'in_progress' && !agentManager.isRunning(taskId)) { + // Clear stale tracking state before starting a new process + taskStateManager.prepareForRestart(taskId); const mainWindow = getMainWindow(); // Check git status before auto-starting @@ -1070,6 +1083,8 @@ export function registerTaskExecutionHandlers( // Auto-restart the task if requested let autoRestarted = false; if (autoRestart) { + // Clear stale tracking state before restarting + taskStateManager.prepareForRestart(taskId); // Check git status before auto-restarting const gitStatusForRestart = checkGitStatus(project.path); if (!gitStatusForRestart.isGitRepo || !gitStatusForRestart.hasCommits) { diff --git a/apps/frontend/src/main/task-state-manager.ts b/apps/frontend/src/main/task-state-manager.ts index e4a979f5f0..fffb7beab0 100644 --- a/apps/frontend/src/main/task-state-manager.ts +++ b/apps/frontend/src/main/task-state-manager.ts @@ -169,6 +169,18 @@ export class TaskStateManager { return this.getCurrentState(taskId) === 'plan_review'; } + /** + * Reset tracking state for a task that is about to be restarted. + * Clears terminalEventSeen (so process exits aren't swallowed) and + * lastSequenceByTask (so events from the new process aren't dropped + * as duplicates). Does NOT stop or remove the XState actor, since + * the caller may still need to send events to it. + */ + prepareForRestart(taskId: string): void { + this.terminalEventSeen.delete(taskId); + this.lastSequenceByTask.delete(taskId); + } + clearTask(taskId: string): void { this.lastSequenceByTask.delete(taskId); this.lastStateByTask.delete(taskId);