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.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user