diff --git a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs index 338d269..a48d3d6 100644 --- a/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs +++ b/src/ClaudeDo.Worker/Planning/PlanningMergeOrchestrator.cs @@ -56,11 +56,9 @@ public sealed class PlanningMergeOrchestrator .Where(c => c.Worktree is not null && c.Worktree.State != WorktreeState.Merged) .Select(c => c.Id)); - _states[planningTaskId] = new State - { - TargetBranch = targetBranch, - RemainingSubtaskIds = queue, - }; + var state = new State { TargetBranch = targetBranch, RemainingSubtaskIds = queue }; + if (!_states.TryAdd(planningTaskId, state)) + throw new InvalidOperationException($"Merge already in progress for {planningTaskId}."); await _broadcaster.PlanningMergeStarted(planningTaskId, targetBranch); await DrainAsync(planningTaskId, ct); @@ -70,38 +68,47 @@ public sealed class PlanningMergeOrchestrator { if (!_states.TryGetValue(planningTaskId, out var state)) return; - while (state.RemainingSubtaskIds.TryDequeue(out var subtaskId)) + var keepState = false; + try { - state.CurrentSubtaskId = subtaskId; - var result = await _merge.MergeAsync( - subtaskId, - state.TargetBranch, - removeWorktree: true, - commitMessage: "Merge subtask", - leaveConflictsInTree: true, - ct); - - if (result.Status == TaskMergeService.StatusConflict) + while (state.RemainingSubtaskIds.TryDequeue(out var subtaskId)) { - await _broadcaster.PlanningMergeConflict(planningTaskId, subtaskId, result.ConflictFiles); - return; + state.CurrentSubtaskId = subtaskId; + var result = await _merge.MergeAsync( + subtaskId, + state.TargetBranch, + removeWorktree: true, + commitMessage: "Merge subtask", + leaveConflictsInTree: true, + ct); + + if (result.Status == TaskMergeService.StatusConflict) + { + await _broadcaster.PlanningMergeConflict(planningTaskId, subtaskId, result.ConflictFiles); + keepState = true; + return; + } + + if (result.Status != TaskMergeService.StatusMerged) + { + await _broadcaster.PlanningMergeConflict( + planningTaskId, subtaskId, + new[] { result.ErrorMessage ?? "merge blocked" }); + keepState = true; + return; + } + + await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId); } - if (result.Status != TaskMergeService.StatusMerged) - { - await _broadcaster.PlanningMergeConflict( - planningTaskId, subtaskId, - new[] { result.ErrorMessage ?? "merge blocked" }); - return; - } - - await _broadcaster.PlanningSubtaskMerged(planningTaskId, subtaskId); + state.CurrentSubtaskId = null; + await FinalizePlanningDoneAsync(planningTaskId, ct); + await _broadcaster.PlanningCompleted(planningTaskId); + } + finally + { + if (!keepState) _states.TryRemove(planningTaskId, out _); } - - state.CurrentSubtaskId = null; - await FinalizePlanningDoneAsync(planningTaskId, ct); - _states.TryRemove(planningTaskId, out _); - await _broadcaster.PlanningCompleted(planningTaskId); } private async Task FinalizePlanningDoneAsync(string planningTaskId, CancellationToken ct)