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.
62 lines
2.6 KiB
C#
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);
|
|
}
|
|
}
|