Merge claudedo/973ea49ec0a642ea9b1ddcff9b71b6fe

This commit is contained in:
mika kuns
2026-08-06 13:47:35 +02:00
10 changed files with 205 additions and 5 deletions
@@ -0,0 +1,10 @@
namespace ClaudeDo.Worker.Planning;
// Lets TaskStateService guard CancelAsync against an in-progress unit merge without a
// hard constructor cycle back to PlanningMergeOrchestrator (which itself depends on
// ITaskStateService) — TaskStateService takes a Func<IActiveMergeState> instead, resolved
// lazily so both singletons can finish constructing before either calls into the other.
public interface IActiveMergeState
{
bool HasActiveMerge(string taskId);
}
@@ -13,7 +13,7 @@ namespace ClaudeDo.Worker.Planning;
/// <summary>A unit-merge conflict currently paused, driven by an MCP session rather than the UI.</summary>
public sealed record ExternalPlanningMergeConflict(string PlanningTaskId, string SubtaskId);
public sealed class PlanningMergeOrchestrator
public sealed class PlanningMergeOrchestrator : IActiveMergeState
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly TaskMergeService _merge;
+4
View File
@@ -101,12 +101,16 @@ builder.Services.AddSingleton<IQueuePicker, QueuePicker>();
builder.Services.AddSingleton<RunCancellationRegistry>();
builder.Services.AddSingleton<Func<ITaskStateService>>(sp => () => sp.GetRequiredService<ITaskStateService>());
// PlanningMergeOrchestrator itself depends on ITaskStateService, so TaskStateService can only
// reach it lazily (Func<IActiveMergeState>) — same cycle-breaking shape as the Func above.
builder.Services.AddSingleton<Func<IActiveMergeState>>(sp => () => sp.GetRequiredService<PlanningMergeOrchestrator>());
builder.Services.AddSingleton<ITaskStateService>(sp => new TaskStateService(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<HubBroadcaster>(),
sp.GetRequiredService<IQueueWaker>(),
sp.GetRequiredService<PlanningChainCoordinator>(),
sp.GetRequiredService<RunCancellationRegistry>(),
sp.GetRequiredService<Func<IActiveMergeState>>(),
sp.GetRequiredService<ILogger<TaskStateService>>()));
// Agent file management.
@@ -16,6 +16,7 @@ public sealed class TaskStateService : ITaskStateService
private readonly IQueueWaker _waker;
private readonly PlanningChainCoordinator _chain;
private readonly RunCancellationRegistry _runCancels;
private readonly Func<IActiveMergeState> _mergeState;
private readonly ILogger<TaskStateService> _logger;
public TaskStateService(
@@ -24,6 +25,7 @@ public sealed class TaskStateService : ITaskStateService
IQueueWaker waker,
PlanningChainCoordinator chain,
RunCancellationRegistry runCancels,
Func<IActiveMergeState> mergeState,
ILogger<TaskStateService> logger)
{
_dbFactory = dbFactory;
@@ -31,6 +33,7 @@ public sealed class TaskStateService : ITaskStateService
_waker = waker;
_chain = chain;
_runCancels = runCancels;
_mergeState = mergeState;
_logger = logger;
}
@@ -255,6 +258,14 @@ public sealed class TaskStateService : ITaskStateService
public async Task<TransitionResult> CancelAsync(
string taskId, DateTime finishedAt, CancellationToken ct, bool allowFromIdle = false)
{
// A unit merge drains its children's worktrees onto the target branch while the parent
// sits in WaitingForReview for the whole (potentially minutes-long) drain. Cancelling out
// from under it would flip the parent to Cancelled while the orchestrator keeps merging —
// FinalizeParentDoneAsync then finds the parent no longer WaitingForReview and gives up,
// leaving the merged children's diffs stranded with no way to roll them back.
if (_mergeState().HasActiveMerge(taskId))
return new TransitionResult(false, "A merge is in progress for this task; wait for it to finish before cancelling.");
List<string> cancelledChildIds;
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
{