feat(worker): surface task numbers in MCP tool return payloads

Adds Number alongside every task id in External/'s DTOs -- the two
central mappers (ToDto/ToRefDto -> TaskDto/TaskRefDto) plus every
DTO that carries a bare task id and bypasses them (batch results,
queue state, wait-for-change, config, attachments, handoff,
lifecycle, merge-preview-set, worktree list). Input resolution
(#123 as an argument) stays for slice 3.
This commit is contained in:
mika kuns
2026-08-11 11:59:51 +02:00
parent 8da1a12389
commit 106c964410
12 changed files with 129 additions and 64 deletions
+26 -9
View File
@@ -7,16 +7,17 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.External;
public sealed record QueueSlotDto(string Slot, string TaskId, DateTime StartedAt);
public sealed record QueueSlotDto(string Slot, string TaskId, int? Number, DateTime StartedAt);
public sealed record QueueWaitReasonDto(string TaskId, string Reason, string BlockedByTaskId);
public sealed record QueueWaitReasonDto(string TaskId, int Number, string Reason, string BlockedByTaskId, int? BlockedByNumber);
public sealed record GetQueueStateResult(
int ConfiguredSlots,
int EffectiveSlots,
IReadOnlyList<QueueSlotDto> ActiveSlots,
IReadOnlyList<string> WaitingTaskIds,
IReadOnlyList<QueueWaitReasonDto> ScopeBlockedTasks);
IReadOnlyList<QueueWaitReasonDto> ScopeBlockedTasks,
IReadOnlyList<int> WaitingTaskNumbers);
[McpServerToolType]
public sealed class QueueStateMcpTools
@@ -44,13 +45,21 @@ public sealed class QueueStateMcpTools
public async Task<GetQueueStateResult> GetQueueState(CancellationToken cancellationToken = default)
{
var (configured, effective) = await _queue.GetSlotCountsAsync(cancellationToken);
var activeSlots = _queue.GetActive()
.Select(a => new QueueSlotDto(a.slot, a.taskId, a.startedAt))
.ToList();
var active = _queue.GetActive();
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
var now = DateTime.UtcNow;
var activeTaskIds = active.Select(a => a.taskId).ToList();
var activeNumbers = await ctx.Tasks
.Where(t => activeTaskIds.Contains(t.Id))
.Select(t => new { t.Id, t.Number })
.ToDictionaryAsync(t => t.Id, t => t.Number, cancellationToken);
var activeSlots = active
.Select(a => new QueueSlotDto(a.slot, a.taskId, activeNumbers.TryGetValue(a.taskId, out var n) ? n : null, a.startedAt))
.ToList();
var waiting = await ctx.Tasks
.Where(t => t.Status == TaskStatus.Queued
&& t.BlockedByTaskId == null
@@ -73,10 +82,18 @@ public sealed class QueueStateMcpTools
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));
{
var blockerNumber = await ctx.Tasks
.Where(b => b.Id == blockerId)
.Select(b => (int?)b.Number)
.FirstOrDefaultAsync(cancellationToken);
scopeBlocked.Add(new QueueWaitReasonDto(t.Id, t.Number, "scope_overlap", blockerId, blockerNumber));
}
}
}
return new GetQueueStateResult(configured, effective, activeSlots, waiting.Select(t => t.Id).ToList(), scopeBlocked);
return new GetQueueStateResult(
configured, effective, activeSlots, waiting.Select(t => t.Id).ToList(), scopeBlocked,
waiting.Select(t => t.Number).ToList());
}
}