Merge branch 'claudedo/5c80a0aba5d9468a96cf809fc7a6e7a1'
This commit is contained in:
+53
-10
@@ -7,7 +7,9 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.External;
|
||||
|
||||
public sealed record TaskStatusChangeDto(string TaskId, string Status);
|
||||
// BlockedReason is set only when Status is "Blocked" -- a Queued task the picker will not
|
||||
// claim yet, either because of a planning-chain predecessor or an unmet depends-on link.
|
||||
public sealed record TaskStatusChangeDto(string TaskId, string Status, string? BlockedReason = null);
|
||||
public sealed record WaitForTaskChangeResult(IReadOnlyList<TaskStatusChangeDto> Changed, bool TimedOut);
|
||||
|
||||
[McpServerToolType]
|
||||
@@ -42,9 +44,12 @@ public sealed class TaskWaitMcpTools
|
||||
[McpServerTool, Description(
|
||||
"Blocks until at least one of the given tasks leaves Queued/Running -- use this instead of " +
|
||||
"polling get_task in a loop. Returns immediately if a task is already outside Queued/Running " +
|
||||
"(an unknown id reports status \"NotFound\" and counts as changed). Pitfall: a planning parent " +
|
||||
"goes Running -> WaitingForChildren while its children are still working, so by default " +
|
||||
"waiting on a parent returns early; see treatWaitingForChildrenAsBusy. Sends MCP progress " +
|
||||
"(an unknown id reports status \"NotFound\" and counts as changed). A Queued task the picker " +
|
||||
"will not claim yet (a planning-chain predecessor, or a depends_on link whose target isn't " +
|
||||
"Done) also reports immediately as status \"Blocked\" with blockedReason set, instead of " +
|
||||
"silently waiting out the full timeout. Pitfall: a planning parent goes Running -> " +
|
||||
"WaitingForChildren while its children are still working, so by default waiting on a parent " +
|
||||
"returns early; see treatWaitingForChildrenAsBusy. Sends MCP progress " +
|
||||
"pings every 30s while waiting so a long wait survives the calling client's own idle-silence " +
|
||||
"abort (Claude Code defaults to killing an MCP call after ~300s of silence) -- this is not " +
|
||||
"guaranteed by every possible MCP client.")]
|
||||
@@ -101,22 +106,60 @@ public sealed class TaskWaitMcpTools
|
||||
var rows = await ctx.Tasks
|
||||
.AsNoTracking()
|
||||
.Where(t => taskIds.Contains(t.Id))
|
||||
.Select(t => new { t.Id, t.Status })
|
||||
.Select(t => new { t.Id, t.Status, t.BlockedByTaskId, t.DependsOnTaskId })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var byId = rows.ToDictionary(r => r.Id, r => r.Status);
|
||||
var byId = rows.ToDictionary(r => r.Id, r => r);
|
||||
|
||||
// A Queued task with an unmet depends_on link never becomes "changed" by itself -- the
|
||||
// picker will not touch it. Batch-resolve those dependencies' statuses once instead of a
|
||||
// query per candidate.
|
||||
var dependencyIds = rows
|
||||
.Where(r => r.Status == TaskStatus.Queued && r.BlockedByTaskId is null && r.DependsOnTaskId is not null)
|
||||
.Select(r => r.DependsOnTaskId!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
var dependencyStatuses = dependencyIds.Count == 0
|
||||
? new Dictionary<string, TaskStatus>()
|
||||
: await ctx.Tasks.AsNoTracking()
|
||||
.Where(t => dependencyIds.Contains(t.Id))
|
||||
.Select(t => new { t.Id, t.Status })
|
||||
.ToDictionaryAsync(t => t.Id, t => t.Status, ct);
|
||||
|
||||
var result = new List<TaskStatusChangeDto>();
|
||||
foreach (var id in taskIds)
|
||||
{
|
||||
if (!byId.TryGetValue(id, out var status))
|
||||
if (!byId.TryGetValue(id, out var row))
|
||||
{
|
||||
result.Add(new TaskStatusChangeDto(id, "NotFound"));
|
||||
continue;
|
||||
}
|
||||
var busy = status == TaskStatus.Queued || status == TaskStatus.Running
|
||||
|| (treatWaitingForChildrenAsBusy && status == TaskStatus.WaitingForChildren);
|
||||
|
||||
if (row.Status == TaskStatus.Queued)
|
||||
{
|
||||
if (row.BlockedByTaskId is not null)
|
||||
{
|
||||
result.Add(new TaskStatusChangeDto(id, "Blocked",
|
||||
$"Blocked by planning-chain predecessor {row.BlockedByTaskId}."));
|
||||
continue;
|
||||
}
|
||||
if (row.DependsOnTaskId is not null)
|
||||
{
|
||||
var known = dependencyStatuses.TryGetValue(row.DependsOnTaskId, out var depStatus);
|
||||
if (!known || depStatus != TaskStatus.Done)
|
||||
{
|
||||
result.Add(new TaskStatusChangeDto(id, "Blocked",
|
||||
$"Blocked: depends on task {row.DependsOnTaskId} (status: " +
|
||||
(known ? depStatus.ToString() : "not found") + ")."));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var busy = row.Status == TaskStatus.Queued || row.Status == TaskStatus.Running
|
||||
|| (treatWaitingForChildrenAsBusy && row.Status == TaskStatus.WaitingForChildren);
|
||||
if (!busy)
|
||||
result.Add(new TaskStatusChangeDto(id, status.ToString()));
|
||||
result.Add(new TaskStatusChangeDto(id, row.Status.ToString()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user