fix: prevent stale duplicate planning tasks from blocking phase completion

Three fixes for phases getting stuck when a detail task crashes and is retried:

1. detailPhase mutation (architect.ts): clean up orphaned pending/in_progress
   detail tasks before creating new ones, preventing duplicates at the source
2. orchestrator recovery: detect and complete stale duplicate planning tasks
   (same category+phase, one completed, one pending)
3. ensureBranch: catch "already exists" TOCTOU race instead of blocking phase
This commit is contained in:
Lukas May
2026-03-06 21:44:26 +01:00
parent ee8c7097db
commit 346d62ef8d
4 changed files with 40 additions and 3 deletions

View File

@@ -20,7 +20,7 @@ import type { ProjectRepository } from '../db/repositories/project-repository.js
import type { AgentRepository } from '../db/repositories/agent-repository.js';
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
import type { ConflictResolutionService } from '../coordination/conflict-resolution-service.js';
import { phaseBranchName, taskBranchName } from '../git/branch-naming.js';
import { phaseBranchName, taskBranchName, isPlanningCategory } from '../git/branch-naming.js';
import { ensureProjectClone } from '../git/project-clones.js';
import { createModuleLogger } from '../logger/index.js';
import { phaseMetaCache, fileDiffCache } from '../review/diff-cache.js';
@@ -637,6 +637,23 @@ export class ExecutionOrchestrator {
}
}
// Clean up stale duplicate planning tasks (e.g. a crashed detail task
// that was reset to pending, then a new detail task was created and completed).
const tasksAfterRecovery = await this.taskRepository.findByPhaseId(phase.id);
const completedPlanningNames = new Set<string>();
for (const t of tasksAfterRecovery) {
if (isPlanningCategory(t.category) && t.status === 'completed') {
completedPlanningNames.add(`${t.category}:${t.phaseId}`);
}
}
for (const t of tasksAfterRecovery) {
if (isPlanningCategory(t.category) && t.status === 'pending' && completedPlanningNames.has(`${t.category}:${t.phaseId}`)) {
await this.taskRepository.update(t.id, { status: 'completed', summary: 'Superseded by retry' });
tasksRecovered++;
log.info({ taskId: t.id, category: t.category }, 'recovered stale duplicate planning task');
}
}
// Re-read tasks after recovery updates and check if phase is now fully done
const updatedTasks = await this.taskRepository.findByPhaseId(phase.id);
const allDone = updatedTasks.every((t) => t.status === 'completed');