fix(worker): make list_tasks/batch_get_tasks lean by default

list_tasks on a list of ~100 verbosely-described tasks could return
390k+ characters in one call, blowing past the caller's token limit.
Both tools now default to lean TaskRefDto references (no
Description/Result) and take an includeDescription flag to opt back
into the full TaskDto payload — same flag-alongside-nullable-payload
idiom already used by BatchGetTaskResult/TaskConfigResult. get_task is
unchanged.
This commit is contained in:
mika kuns
2026-08-06 11:25:32 +02:00
parent 86f962ee7d
commit f8c48e2ed7
5 changed files with 151 additions and 22 deletions
+22 -8
View File
@@ -6,7 +6,10 @@ namespace ClaudeDo.Worker.External;
public sealed record BatchAddTaskInput(string Title, string? Description = null, string? Model = null);
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
public sealed record BatchGetTaskResult(string Id, bool Found, TaskDto? Task, string? Error);
// task is populated when found and includeDescription=false (the default, lean reference);
// taskFull is populated when found and includeDescription=true (full task incl.
// Description/Result). Both are null when found=false.
public sealed record BatchGetTaskResult(string Id, bool Found, TaskRefDto? Task, TaskDto? TaskFull, string? Error);
public sealed record BatchAddTaskResult(int Index, string Title, bool Ok, TaskRefDto? Task, string? Error);
public sealed record BatchTaskResult(string TaskId, bool Ok, string? Error);
public sealed record BatchCancelResult(string TaskId, bool Ok, bool Cancelled, string? Error);
@@ -30,10 +33,13 @@ public sealed class BatchMcpTools
[McpServerTool, Description(
"Fetch a snapshot of many tasks in one call (overview / polling a fan-out). " +
"Returns one result per id: { id, found, task, error }. A missing id is found=false " +
"(not an error); error is only set for an unexpected failure. Max 100 ids.")]
"Returns one result per id: { id, found, task, taskFull, error }. " +
"includeDescription=false (default): found tasks come back in `task` (lean reference, no " +
"Description/Result). includeDescription=true: found tasks come back in `taskFull` (incl. " +
"Description/Result) instead. A missing id is found=false (not an error; task and taskFull both null); " +
"error is only set for an unexpected failure. Max 100 ids.")]
public async Task<IReadOnlyList<BatchGetTaskResult>> BatchGetTasks(
string[] taskIds, CancellationToken cancellationToken)
string[] taskIds, bool includeDescription = false, CancellationToken cancellationToken = default)
{
EnsureWithinCap(taskIds, nameof(taskIds));
@@ -42,17 +48,25 @@ public sealed class BatchMcpTools
{
try
{
var task = await _svc.GetTask(id, cancellationToken);
results.Add(new BatchGetTaskResult(id, true, task, null));
if (includeDescription)
{
var task = await _svc.GetTask(id, cancellationToken);
results.Add(new BatchGetTaskResult(id, true, null, task, null));
}
else
{
var taskRef = await _svc.GetTaskRefAsync(id, cancellationToken);
results.Add(new BatchGetTaskResult(id, true, taskRef, null, null));
}
}
catch (OperationCanceledException) { throw; }
catch (InvalidOperationException)
{
results.Add(new BatchGetTaskResult(id, false, null, null));
results.Add(new BatchGetTaskResult(id, false, null, null, null));
}
catch (Exception ex)
{
results.Add(new BatchGetTaskResult(id, false, null, ex.Message));
results.Add(new BatchGetTaskResult(id, false, null, null, ex.Message));
}
}
return results;
+30 -3
View File
@@ -47,6 +47,15 @@ public sealed record TaskRefDto(
int SortOrder,
bool IsMyDay);
// tasks is populated when includeDescription=false (the default): lean references, no
// Description/Result. tasksFull is populated when includeDescription=true: full tasks incl.
// Description/Result. Exactly one of the two is non-null per the includeDescription flag —
// same "flag alongside nullable payload" idiom as BatchGetTaskResult.
public sealed record ListTasksResult(
bool IncludeDescription,
IReadOnlyList<TaskRefDto>? Tasks,
IReadOnlyList<TaskDto>? TasksFull);
public sealed record WorktreeInfoDto(
string Path, string Branch, string HeadCommit, string BaseCommit,
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
@@ -139,11 +148,17 @@ public sealed class ExternalMcpService
[McpServerTool, Description(
"List tasks in a given list. Optionally filter by creator (createdBy) and/or status. " +
"Valid status values: Idle, Queued, Running, WaitingForReview, WaitingForChildren, Done, Failed, Cancelled.")]
public async Task<IReadOnlyList<TaskDto>> ListTasks(
"Valid status values: Idle, Queued, Running, WaitingForReview, WaitingForChildren, Done, Failed, Cancelled. " +
"includeDescription=false (default): returns lean task references in `tasks` (no Description/Result) — " +
"use this unless you actually need the description text, since a list of verbosely-described tasks can " +
"otherwise blow past the response size limit. " +
"includeDescription=true: returns full tasks (incl. Description/Result) in `tasksFull` instead; `tasks` is " +
"null in that case.")]
public async Task<ListTasksResult> ListTasks(
string listId,
string? createdBy = null,
string? status = null,
bool includeDescription = false,
CancellationToken cancellationToken = default)
{
TaskStatus? statusFilter = null;
@@ -162,7 +177,10 @@ public sealed class ExternalMcpService
if (statusFilter is not null)
query = query.Where(t => t.Status == statusFilter);
return query.Select(ToDto).ToList();
var filtered = query.ToList();
return includeDescription
? new ListTasksResult(true, null, filtered.Select(ToDto).ToList())
: new ListTasksResult(false, filtered.Select(ToRefDto).ToList(), null);
}
[McpServerTool, Description(
@@ -177,6 +195,15 @@ public sealed class ExternalMcpService
return ToDto(task);
}
// Lean counterpart to GetTask, used internally by BatchGetTasks' default (includeDescription=false)
// path. Not an MCP tool itself — GetTask's own behavior stays untouched.
internal async Task<TaskRefDto> GetTaskRefAsync(string taskId, CancellationToken cancellationToken)
{
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
?? throw new InvalidOperationException($"Task {taskId} not found.");
return ToRefDto(task);
}
[McpServerTool, Description(
"Create a new task in the given list. Set queueImmediately=true to enqueue it for agent execution. " +
"Set model to the cheapest model that can do the task well — 'haiku' for trivial/mechanical work, " +