Skip to content

Commit 28a6200

Browse files
AndyMik90claude
andauthored
fix: clear terminalEventSeen on task restart to prevent stuck-after-planning (#1828) (#1840)
* fix: clear terminalEventSeen on task restart to prevent stuck-after-planning (#1828) The terminalEventSeen Set in TaskStateManager was never cleared when a task was restarted. When spec_runner.py emits PLANNING_COMPLETE, the taskId is added to terminalEventSeen. If the subsequent coding process (run.py) fails, handleProcessExited() returns early because terminalEventSeen.has(taskId) is true, silently swallowing the PROCESS_EXITED event. The XState actor never transitions, leaving the task permanently stuck in 'coding' state. Additionally, lastSequenceByTask from the old process would cause events from a new process (starting at sequence 0) to be dropped as duplicates. Fix: Add prepareForRestart(taskId) method that clears both terminalEventSeen and lastSequenceByTask without stopping the XState actor. Call it in all 4 locations where a new agent process is started: - TASK_START handler - TASK_STOP handler (so subsequent restart works) - TASK_UPDATE_STATUS auto-start path - TASK_RECOVER_STUCK auto-restart path Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add prepareForRestart to TASK_REVIEW rejection path Add missing prepareForRestart(taskId) call before startQAProcess() in the TASK_REVIEW rejection handler. This is the 5th location where a new agent process is started for an existing task, but was missed in the original fix. Without this, if the QA fixer process crashes after a review rejection, terminalEventSeen would cause handleProcessExited() to swallow the exit event, leaving the task permanently stuck. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fb3a3fb commit 28a6200

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

apps/frontend/src/main/ipc-handlers/task/execution-handlers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ export function registerTaskExecutionHandlers(
177177

178178
console.warn('[TASK_START] Found task:', task.specId, 'status:', task.status, 'reviewReason:', task.reviewReason, 'subtasks:', task.subtasks.length);
179179

180+
// Clear stale tracking state from any previous execution so that:
181+
// - terminalEventSeen doesn't suppress future PROCESS_EXITED events
182+
// - lastSequenceByTask doesn't drop events from the new process
183+
taskStateManager.prepareForRestart(taskId);
184+
180185
// Check if implementation_plan.json has valid subtasks BEFORE XState handling.
181186
// This is more reliable than task.subtasks.length which may not be loaded yet.
182187
const specsBaseDir = getSpecsDir(project.autoBuildPath);
@@ -360,6 +365,9 @@ export function registerTaskExecutionHandlers(
360365
task,
361366
project
362367
);
368+
369+
// Clear stale tracking state so a subsequent restart works correctly
370+
taskStateManager.prepareForRestart(taskId);
363371
});
364372

365373
/**
@@ -529,6 +537,9 @@ export function registerTaskExecutionHandlers(
529537
return { success: false, error: 'Failed to write QA fix request file' };
530538
}
531539

540+
// Clear stale tracking state before starting new QA process
541+
taskStateManager.prepareForRestart(taskId);
542+
532543
// Restart QA process - use worktree path if it exists, otherwise main project
533544
// The QA process needs to run where the implementation_plan.json with completed subtasks is
534545
const qaProjectPath = hasWorktree ? worktreePath : project.path;
@@ -703,6 +714,8 @@ export function registerTaskExecutionHandlers(
703714

704715
// Auto-start task when status changes to 'in_progress' and no process is running
705716
if (status === 'in_progress' && !agentManager.isRunning(taskId)) {
717+
// Clear stale tracking state before starting a new process
718+
taskStateManager.prepareForRestart(taskId);
706719
const mainWindow = getMainWindow();
707720

708721
// Check git status before auto-starting
@@ -1133,6 +1146,8 @@ export function registerTaskExecutionHandlers(
11331146
// Auto-restart the task if requested
11341147
let autoRestarted = false;
11351148
if (autoRestart) {
1149+
// Clear stale tracking state before restarting
1150+
taskStateManager.prepareForRestart(taskId);
11361151
// Check git status before auto-restarting
11371152
const gitStatusForRestart = checkGitStatus(project.path);
11381153
if (!gitStatusForRestart.isGitRepo || !gitStatusForRestart.hasCommits) {

apps/frontend/src/main/task-state-manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,18 @@ export class TaskStateManager {
169169
return this.getCurrentState(taskId) === 'plan_review';
170170
}
171171

172+
/**
173+
* Reset tracking state for a task that is about to be restarted.
174+
* Clears terminalEventSeen (so process exits aren't swallowed) and
175+
* lastSequenceByTask (so events from the new process aren't dropped
176+
* as duplicates). Does NOT stop or remove the XState actor, since
177+
* the caller may still need to send events to it.
178+
*/
179+
prepareForRestart(taskId: string): void {
180+
this.terminalEventSeen.delete(taskId);
181+
this.lastSequenceByTask.delete(taskId);
182+
}
183+
172184
clearTask(taskId: string): void {
173185
this.lastSequenceByTask.delete(taskId);
174186
this.lastStateByTask.delete(taskId);

0 commit comments

Comments
 (0)