feat(claude-do): feat(queue): Option pro Liste — Tasks mit überlappendem Date
## Befund (Batch-Lauf 2026-08-06, Liste "Bandel.Hub") maxParallelExecutions = 5, 23 Geschwister-Tasks auf demselben Plugin. Die Parallelitaet hat die Ausfuehrung verkuerzt, aber die Konfliktlast erhoeht: eine CSS-Datei wurde von 10 Tasks angefasst, drei Razor-Dateien von je 6. Der Engpass des Gesamtdurchlaufs war nicht die Ausfuehrung, sondern Review und Merge — die sind zwingend seriell. Netto wa ClaudeDo-Task: 89f989e0-4fcf-43f5-a802-11d63df7a6d1
This commit is contained in:
+29
-5
@@ -9,11 +9,14 @@ namespace ClaudeDo.Worker.External;
|
||||
|
||||
public sealed record QueueSlotDto(string Slot, string TaskId, DateTime StartedAt);
|
||||
|
||||
public sealed record QueueWaitReasonDto(string TaskId, string Reason, string BlockedByTaskId);
|
||||
|
||||
public sealed record GetQueueStateResult(
|
||||
int ConfiguredSlots,
|
||||
int EffectiveSlots,
|
||||
IReadOnlyList<QueueSlotDto> ActiveSlots,
|
||||
IReadOnlyList<string> WaitingTaskIds);
|
||||
IReadOnlyList<string> WaitingTaskIds,
|
||||
IReadOnlyList<QueueWaitReasonDto> ScopeBlockedTasks);
|
||||
|
||||
[McpServerToolType]
|
||||
public sealed class QueueStateMcpTools
|
||||
@@ -33,7 +36,11 @@ public sealed class QueueStateMcpTools
|
||||
"by the usage throttle (lower when the 5h/7d usage window fills up), so comparing the two " +
|
||||
"shows whether throttling is currently active. Each active slot is \"queue\" (a normal " +
|
||||
"queue slot) or \"override\" (the single run_task_now/continue_task slot). waitingTaskIds " +
|
||||
"lists queued, unblocked, non-manual, due tasks in the order the queue would pick them next.")]
|
||||
"lists queued, unblocked, non-manual, due tasks in the order the queue would pick them next -- " +
|
||||
"including any held back purely by file-scope overlap, which is why a waiting task can " +
|
||||
"outlast a free slot. scopeBlockedTasks explains those: the list opted into " +
|
||||
"serializeOnFileOverlap and this task's declared scope overlaps blockedByTaskId, a running " +
|
||||
"or awaiting-merge sibling in the same list.")]
|
||||
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
|
||||
@@ -44,15 +51,32 @@ public sealed class QueueStateMcpTools
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
|
||||
var now = DateTime.UtcNow;
|
||||
var waitingTaskIds = await ctx.Tasks
|
||||
var waiting = await ctx.Tasks
|
||||
.Where(t => t.Status == TaskStatus.Queued
|
||||
&& t.BlockedByTaskId == null
|
||||
&& !t.IsManual
|
||||
&& (t.ScheduledFor == null || t.ScheduledFor <= now))
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetQueueStateResult(configured, effective, activeSlots, waitingTaskIds);
|
||||
var serializingListIds = (await ctx.ListConfigs
|
||||
.Where(c => c.SerializeOnFileOverlap)
|
||||
.Select(c => c.ListId)
|
||||
.ToListAsync(cancellationToken))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
var scopeBlocked = new List<QueueWaitReasonDto>();
|
||||
if (serializingListIds.Count > 0)
|
||||
{
|
||||
foreach (var t in waiting)
|
||||
{
|
||||
if (!serializingListIds.Contains(t.ListId)) continue;
|
||||
var blockerId = await ScopeOverlap.FindBlockingSiblingAsync(ctx, t, cancellationToken);
|
||||
if (blockerId is not null)
|
||||
scopeBlocked.Add(new QueueWaitReasonDto(t.Id, "scope_overlap", blockerId));
|
||||
}
|
||||
}
|
||||
|
||||
return new GetQueueStateResult(configured, effective, activeSlots, waiting.Select(t => t.Id).ToList(), scopeBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user