feat(planning): consolidate finalize+chain via TaskStateService, fix queue pickup
Slice 4 of the worker state consolidation refactor. Eliminates the "queue never picks up planning tasks" bug structurally by routing both the manager and MCP finalize paths through TaskStateService and PlanningChainCoordinator.SetupChainAsync, where the auto-wake on enqueue guarantees the queue picker claims the first child immediately. - Delete TaskRepository.FinalizePlanningAsync; PlanningSessionManager now orchestrates via _state.FinalizePlanningAsync + _chain.SetupChainAsync. - Rename QueueSubtasksSequentiallyAsync to SetupChainAsync (internal); layout is now Status=Queued + BlockedByTaskId, with auto-attached agent tag. - OnChildFinishedAsync looks up the successor by BlockedByTaskId, drops the legacy Waiting status lookup. - PlanningMcpService.Finalize routes through state+chain; EditableStatuses drops Waiting and adds Idle; gate uses PlanningPhase==Active. - TaskStateService.FinalizePlanningAsync clears the planning session token. - UI: TaskRowViewModel adds BlockedByTaskId; IsQueued/IsWaiting reflect the new layout; TasksIslandViewModel.RemoveFromQueueAsync clears BlockedByTaskId on dequeue. - New regression test PlanningEndToEndTests.FinalizeAsync_FirstChildIs ClaimedByPicker_WithinDeadline asserts the picker claims the first child within 200ms with no manual WakeQueue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
064a903076
commit
4ab906ff0b
@@ -2,6 +2,7 @@ using System.ComponentModel;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.State;
|
||||
using ModelContextProtocol.Server;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
@@ -16,15 +17,21 @@ public sealed class PlanningMcpService
|
||||
private readonly TaskRepository _tasks;
|
||||
private readonly PlanningMcpContextAccessor _contextAccessor;
|
||||
private readonly HubBroadcaster _broadcaster;
|
||||
private readonly ITaskStateService _state;
|
||||
private readonly PlanningChainCoordinator _chain;
|
||||
|
||||
public PlanningMcpService(
|
||||
TaskRepository tasks,
|
||||
PlanningMcpContextAccessor contextAccessor,
|
||||
HubBroadcaster broadcaster)
|
||||
HubBroadcaster broadcaster,
|
||||
ITaskStateService state,
|
||||
PlanningChainCoordinator chain)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_contextAccessor = contextAccessor;
|
||||
_broadcaster = broadcaster;
|
||||
_state = state;
|
||||
_chain = chain;
|
||||
}
|
||||
|
||||
private Task BroadcastTaskUpdatedAsync(string taskId, CancellationToken ct)
|
||||
@@ -61,9 +68,9 @@ public sealed class PlanningMcpService
|
||||
}
|
||||
|
||||
private static readonly TaskStatus[] EditableStatuses =
|
||||
{ TaskStatus.Draft, TaskStatus.Manual, TaskStatus.Queued, TaskStatus.Waiting };
|
||||
{ TaskStatus.Draft, TaskStatus.Idle, TaskStatus.Manual, TaskStatus.Queued };
|
||||
|
||||
[McpServerTool, Description("Update a child task in the active planning session. Can change title, description, tags (replaces the full set), commit type, and status. Status must be one of: Draft, Manual, Queued, Waiting.")]
|
||||
[McpServerTool, Description("Update a child task in the active planning session. Can change title, description, tags (replaces the full set), commit type, and status. Status must be one of: Draft, Idle, Manual, Queued.")]
|
||||
public async Task<ChildTaskDto> UpdateChildTask(
|
||||
string taskId,
|
||||
string? title,
|
||||
@@ -76,7 +83,7 @@ public sealed class PlanningMcpService
|
||||
var ctx = _contextAccessor.Current;
|
||||
var parent = await _tasks.GetByIdAsync(ctx.ParentTaskId, cancellationToken)
|
||||
?? throw new InvalidOperationException("Planning parent task not found.");
|
||||
if (parent.Status != TaskStatus.Planning)
|
||||
if (parent.PlanningPhase != PlanningPhase.Active)
|
||||
throw new InvalidOperationException("Cannot modify tasks outside an active planning session.");
|
||||
|
||||
var child = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
@@ -90,7 +97,7 @@ public sealed class PlanningMcpService
|
||||
if (!Enum.TryParse<TaskStatus>(status, ignoreCase: true, out var parsed))
|
||||
throw new InvalidOperationException($"Unknown status '{status}'.");
|
||||
if (!EditableStatuses.Contains(parsed))
|
||||
throw new InvalidOperationException($"Status '{parsed}' cannot be set via MCP. Allowed: Draft, Manual, Queued, Waiting.");
|
||||
throw new InvalidOperationException($"Status '{parsed}' cannot be set via MCP. Allowed: Draft, Idle, Manual, Queued.");
|
||||
newStatus = parsed;
|
||||
}
|
||||
|
||||
@@ -111,7 +118,7 @@ public sealed class PlanningMcpService
|
||||
var ctx = _contextAccessor.Current;
|
||||
var parent = await _tasks.GetByIdAsync(ctx.ParentTaskId, cancellationToken)
|
||||
?? throw new InvalidOperationException("Planning parent task not found.");
|
||||
if (parent.Status != TaskStatus.Planning)
|
||||
if (parent.PlanningPhase != PlanningPhase.Active)
|
||||
throw new InvalidOperationException("Cannot delete tasks outside an active planning session.");
|
||||
|
||||
var child = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
@@ -141,11 +148,19 @@ public sealed class PlanningMcpService
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ctx = _contextAccessor.Current;
|
||||
var childIds = (await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken))
|
||||
.Select(c => c.Id).ToList();
|
||||
var count = await _tasks.FinalizePlanningAsync(ctx.ParentTaskId, queueAgentTasks, cancellationToken);
|
||||
foreach (var id in childIds)
|
||||
await BroadcastTaskUpdatedAsync(id, cancellationToken);
|
||||
|
||||
var finalizeResult = await _state.FinalizePlanningAsync(ctx.ParentTaskId, cancellationToken);
|
||||
if (!finalizeResult.Ok)
|
||||
throw new InvalidOperationException(
|
||||
finalizeResult.Reason ?? $"Could not finalize planning for task {ctx.ParentTaskId}.");
|
||||
|
||||
var children = await _tasks.GetChildrenAsync(ctx.ParentTaskId, cancellationToken);
|
||||
int count = children.Count;
|
||||
if (queueAgentTasks && children.Count > 0)
|
||||
count = await _chain.SetupChainAsync(ctx.ParentTaskId, cancellationToken);
|
||||
|
||||
foreach (var c in children)
|
||||
await BroadcastTaskUpdatedAsync(c.Id, cancellationToken);
|
||||
await BroadcastTaskUpdatedAsync(ctx.ParentTaskId, cancellationToken);
|
||||
return count;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user