perf(worker): stop echoing task description from writing MCP tools
update_task, update_task_status, add_task, add_subtask, set_my_day, abort_merge, review_task, and their batch variants now return a lean TaskRefDto (id/listId/title/status/sortOrder/isMyDay) instead of the full TaskDto. Those tools were re-sending the caller's own description text back on every call, wasting a large share of session context on list-handler-style runs. get_task/list_tasks/batch_get_tasks are untouched and still return the full DTO.
This commit is contained in:
+3
-2
@@ -7,7 +7,7 @@ public sealed record BatchAddTaskInput(string Title, string? Description = null,
|
||||
public sealed record BatchSetMyDayInput(string TaskId, bool IsMyDay, int? SortOrder = null);
|
||||
|
||||
public sealed record BatchGetTaskResult(string Id, bool Found, TaskDto? Task, string? Error);
|
||||
public sealed record BatchAddTaskResult(int Index, string Title, bool Ok, TaskDto? Task, 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);
|
||||
public sealed record BatchCleanupResult(string TaskId, bool Ok, bool Removed, bool BranchDeleted, string? Error);
|
||||
@@ -62,7 +62,8 @@ public sealed class BatchMcpTools
|
||||
"Create many tasks in one list at once. Each item: { title, description?, model? } " +
|
||||
"(model: haiku|sonnet|opus, blank = inherit list/global default). " +
|
||||
"queueImmediately enqueues every created task. " +
|
||||
"Returns one result per item: { index, title, ok, task, error }. Max 100 items.")]
|
||||
"Returns one result per item: { index, title, ok, task, error }; task is a lean reference " +
|
||||
"(id, listId, title, status, sortOrder, isMyDay), not the description you just sent. Max 100 items.")]
|
||||
public async Task<IReadOnlyList<BatchAddTaskResult>> BatchAddTasks(
|
||||
string listId,
|
||||
BatchAddTaskInput[] tasks,
|
||||
|
||||
+48
-21
@@ -19,7 +19,7 @@ namespace ClaudeDo.Worker.External;
|
||||
public sealed record TaskListDto(string Id, string Name, string? WorkingDir);
|
||||
public sealed record DeleteTaskResult(bool Deleted, string Id);
|
||||
public sealed record CancelTaskResult(bool Cancelled, string Id);
|
||||
public sealed record ReviewTaskResult(TaskDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
||||
public sealed record ReviewTaskResult(TaskRefDto Task, string? MergeStatus, IReadOnlyList<string> MergeConflicts, string? MergeMessage, string? RepoPath = null);
|
||||
public sealed record StatusValueDto(string Status, string Meaning);
|
||||
public sealed record RunTaskNowResult(bool Started, string TaskId);
|
||||
|
||||
@@ -37,6 +37,16 @@ public sealed record TaskDto(
|
||||
bool IsMyDay,
|
||||
int SortOrder);
|
||||
|
||||
// Lean counterpart to TaskDto for writing/status-changing tools: echoes back what changed
|
||||
// without re-sending Description/Result, which the caller just sent or already has.
|
||||
public sealed record TaskRefDto(
|
||||
string Id,
|
||||
string ListId,
|
||||
string Title,
|
||||
string Status,
|
||||
int SortOrder,
|
||||
bool IsMyDay);
|
||||
|
||||
public sealed record WorktreeInfoDto(
|
||||
string Path, string Branch, string HeadCommit, string BaseCommit,
|
||||
int Ahead, int Behind, bool IsDirty, string? MergeCommit = null);
|
||||
@@ -171,8 +181,9 @@ public sealed class ExternalMcpService
|
||||
"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, " +
|
||||
"'sonnet' for normal coding (the default), 'opus' only for complex or cross-cutting work. " +
|
||||
"Leave model null to inherit the list/global default.")]
|
||||
public async Task<TaskDto> AddTask(
|
||||
"Leave model null to inherit the list/global default. " +
|
||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay) — not the description you just sent.")]
|
||||
public async Task<TaskRefDto> AddTask(
|
||||
string listId,
|
||||
string title,
|
||||
string? description = null,
|
||||
@@ -214,11 +225,14 @@ public sealed class ExternalMcpService
|
||||
}
|
||||
|
||||
await _broadcaster.TaskUpdated(entity.Id);
|
||||
return ToDto(entity);
|
||||
return ToRefDto(entity);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. Refuses if the task is currently Running.")]
|
||||
public async Task<TaskDto> UpdateTask(
|
||||
[McpServerTool, Description(
|
||||
"Update an existing task's title, description, and/or commit type. Pass null to leave a field unchanged. " +
|
||||
"Refuses if the task is currently Running. Returns a lean task reference (id, listId, title, status, " +
|
||||
"sortOrder, isMyDay) — not the description you just sent.")]
|
||||
public async Task<TaskRefDto> UpdateTask(
|
||||
string taskId,
|
||||
string? title = null,
|
||||
string? description = null,
|
||||
@@ -237,13 +251,14 @@ public sealed class ExternalMcpService
|
||||
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(reload);
|
||||
return ToRefDto(reload);
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
"Append a subtask (step) to a task. orderNum defaults to the end. " +
|
||||
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list.")]
|
||||
public async Task<TaskDto> AddSubtask(
|
||||
"Refuses if the task is currently Running. Subtasks are surfaced to the agent at run time and shown in the task's Steps list. " +
|
||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||
public async Task<TaskRefDto> AddSubtask(
|
||||
string taskId,
|
||||
string title,
|
||||
int? orderNum = null,
|
||||
@@ -275,7 +290,7 @@ public sealed class ExternalMcpService
|
||||
}, cancellationToken);
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(task);
|
||||
return ToRefDto(task);
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -285,8 +300,9 @@ public sealed class ExternalMcpService
|
||||
"Cancelled (retire the task without deleting it; it can be reset to Idle later), " +
|
||||
"Done (mark complete; refused if the task has an active worktree — use review_task to approve " +
|
||||
"and merge that worktree instead). " +
|
||||
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled.")]
|
||||
public async Task<TaskDto> UpdateTaskStatus(
|
||||
"Full lifecycle: Idle → Queued → Running → WaitingForReview → Done | Failed | Cancelled. " +
|
||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||
public async Task<TaskRefDto> UpdateTaskStatus(
|
||||
string taskId,
|
||||
string status,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -337,7 +353,7 @@ public sealed class ExternalMcpService
|
||||
}
|
||||
|
||||
var reload = (await _tasks.GetByIdAsync(taskId, cancellationToken))!;
|
||||
return ToDto(reload);
|
||||
return ToRefDto(reload);
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -352,7 +368,8 @@ public sealed class ExternalMcpService
|
||||
"decision='reject_rerun' → Queued and re-runs, resuming the agent's session with your feedback as the next turn (feedback is required). " +
|
||||
"decision='reject_park' → Idle for manual editing (feedback ignored). " +
|
||||
"decision='cancel' → Cancelled. " +
|
||||
"Fails if the task is not currently WaitingForReview (except cancel, which also works while Running/Queued).")]
|
||||
"Fails if the task is not currently WaitingForReview (except cancel, which also works while Running/Queued). " +
|
||||
"The result's task field is a lean reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||
public async Task<ReviewTaskResult> ReviewTask(
|
||||
string taskId,
|
||||
string decision,
|
||||
@@ -433,7 +450,7 @@ public sealed class ExternalMcpService
|
||||
}
|
||||
|
||||
return new ReviewTaskResult(
|
||||
ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!),
|
||||
ToRefDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!),
|
||||
mergeStatus, mergeConflicts, mergeMessage, repoPath);
|
||||
}
|
||||
|
||||
@@ -725,8 +742,9 @@ public sealed class ExternalMcpService
|
||||
"Abort an in-progress conflicted merge, discarding the conflict markers and restoring a clean working tree. " +
|
||||
"Handles both a single task's merge and a parent/children unit merge — pass the PARENT task id to abort a " +
|
||||
"unit merge. The task keeps its pre-merge status (e.g. WaitingForReview). " +
|
||||
"Throws if there is no in-progress merge for the task.")]
|
||||
public async Task<TaskDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
||||
"Throws if there is no in-progress merge for the task. " +
|
||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||
public async Task<TaskRefDto> AbortMerge(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
@@ -743,7 +761,7 @@ public sealed class ExternalMcpService
|
||||
}
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
||||
return ToRefDto((await _tasks.GetByIdAsync(taskId, cancellationToken))!);
|
||||
}
|
||||
|
||||
[McpServerTool, Description(
|
||||
@@ -997,8 +1015,9 @@ public sealed class ExternalMcpService
|
||||
"Daily prep: set or clear a task's MyDay flag, optionally setting its sortOrder " +
|
||||
"(use consecutive sortOrder values to keep related tasks together). " +
|
||||
"Setting isMyDay=true is rejected if it would exceed the MyDay cap (DailyPrepMaxTasks open MyDay tasks); " +
|
||||
"clearing (isMyDay=false) is always allowed.")]
|
||||
public async Task<TaskDto> SetMyDay(
|
||||
"clearing (isMyDay=false) is always allowed. " +
|
||||
"Returns a lean task reference (id, listId, title, status, sortOrder, isMyDay), not the task's description.")]
|
||||
public async Task<TaskRefDto> SetMyDay(
|
||||
string taskId,
|
||||
bool isMyDay,
|
||||
int? sortOrder = null,
|
||||
@@ -1025,7 +1044,7 @@ public sealed class ExternalMcpService
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
return ToDto(task);
|
||||
return ToRefDto(task);
|
||||
}
|
||||
|
||||
private static DailyPrepCandidateDto ToCandidate(TaskEntity t) => new(
|
||||
@@ -1120,6 +1139,14 @@ public sealed class ExternalMcpService
|
||||
t.FinishedAt,
|
||||
t.IsMyDay,
|
||||
t.SortOrder);
|
||||
|
||||
private static TaskRefDto ToRefDto(TaskEntity t) => new(
|
||||
t.Id,
|
||||
t.ListId,
|
||||
t.Title,
|
||||
t.Status.ToString(),
|
||||
t.SortOrder,
|
||||
t.IsMyDay);
|
||||
}
|
||||
|
||||
internal static class DailyPrepFilter
|
||||
|
||||
@@ -189,6 +189,41 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
Assert.Equal("new title", loaded!.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTask_ReturnsLeanReference_NotFullTaskDto()
|
||||
{
|
||||
// TaskRefDto has no Description/Result properties -- a writing tool that echoes it back
|
||||
// would re-send the (possibly long) description the caller just sent.
|
||||
Assert.DoesNotContain(nameof(TaskDto.Description), typeof(TaskRefDto).GetProperties().Select(p => p.Name));
|
||||
Assert.DoesNotContain("Result", typeof(TaskRefDto).GetProperties().Select(p => p.Name));
|
||||
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, "old title");
|
||||
task.Description = "a long description the caller already has";
|
||||
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.UpdateTask(task.Id, "new title", null, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(task.Id, dto.Id);
|
||||
Assert.Equal(listId, dto.ListId);
|
||||
Assert.Equal("new title", dto.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTask_ReturnsFullDtoIncludingDescription()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, "with description");
|
||||
task.Description = "the full description text";
|
||||
await _tasks.UpdateAsync(task, CancellationToken.None);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.GetTask(task.Id, CancellationToken.None);
|
||||
|
||||
Assert.Equal("the full description text", dto.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateTask_OnRunning_Throws()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user