Files
ClaudeDo/src/ClaudeDo.Worker/External/QueueStateMcpTools.cs
T
mika kuns d43b5fcefc feat(worker-mcp): raise wait_for_task_change timeout, expose queue slot state
MaxTimeoutSeconds was 170s against runs that take tens of minutes, forcing
a dozen full-context wait rounds per long-running batch. Raise it to 900s
and raise MCP_TOOL_TIMEOUT in lockstep (ClaudeProcess + every
InteractiveLaunchSpecService launch spec) to 930000ms so the client
connection actually stays open that long instead of aborting first.

Add get_queue_state (QueueStateMcpTools): configured vs. effective
parallel-slot count (via QueueService.GetSlotCountsAsync, extracted from
the former GetEffectiveMaxParallelAsync), active slots with taskId +
startedAt including the run_task_now override slot, and queued tasks in
pick order -- so a caller can observe queue occupancy instead of inferring
it from maxParallelExecutions.
2026-08-05 20:47:57 +02:00

62 lines
2.6 KiB
C#

using System.ComponentModel;
using ClaudeDo.Data;
using ClaudeDo.Worker.Queue;
using Microsoft.EntityFrameworkCore;
using ModelContextProtocol.Server;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.External;
public sealed record QueueSlotDto(string Slot, string TaskId, DateTime StartedAt);
public sealed record GetQueueStateResult(
int ConfiguredSlots,
int EffectiveSlots,
IReadOnlyList<QueueSlotDto> ActiveSlots,
IReadOnlyList<string> WaitingTaskIds);
[McpServerToolType]
public sealed class QueueStateMcpTools
{
private readonly QueueService _queue;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
public QueueStateMcpTools(QueueService queue, IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_queue = queue;
_dbFactory = dbFactory;
}
[McpServerTool, Description(
"Read-only snapshot of the execution queue -- observe slot occupancy instead of inferring " +
"it from maxParallelExecutions. Result: { configuredSlots, effectiveSlots, activeSlots: " +
"[{ slot, taskId, startedAt }], waitingTaskIds }. configuredSlots is Settings -> " +
"MaxParallelExecutions; effectiveSlots is that value stepped down by the usage throttle " +
"(lower when the 5h/7d usage window is filling up) -- compare the two to see whether " +
"throttling is currently active. activeSlots lists every task presently holding an " +
"execution slot, with slot \"queue\" for a normal queue slot or \"override\" for 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.")]
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();
await using var ctx = await _dbFactory.CreateDbContextAsync(cancellationToken);
var now = DateTime.UtcNow;
var waitingTaskIds = 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);
}
}