Merge branch 'claudedo/16172942c1424a67a0499e01cefb7d60'
This commit is contained in:
+2
-1
@@ -83,7 +83,8 @@ public sealed class BatchMcpTools
|
||||
[McpServerTool, Description(
|
||||
"Fetch a snapshot of many tasks in one call — use for an overview or polling a fan-out instead of " +
|
||||
"calling get_task per id. A missing id comes back as found=false, not an error; error is only set " +
|
||||
"for an unexpected failure. When includeDescription=true, Description/Result are cut to " +
|
||||
"for an unexpected failure. A Failed task's task/taskFull carries failureReason (see get_task). " +
|
||||
"When includeDescription=true, Description/Result are cut to " +
|
||||
"descriptionMaxChars (default 1500 — was unlimited); check *Truncated/*FullLength before assuming " +
|
||||
"you got the whole text. Use `fields` to fetch only what you need (e.g. just roadblockText) instead " +
|
||||
"of raising the cap. The whole response is still capped — if it's too big even so, lower " +
|
||||
|
||||
+26
-5
@@ -45,7 +45,13 @@ public sealed record TaskDto(
|
||||
// A planning/improvement child reporting > 0 still goes straight to Done (see
|
||||
// ClaudeDo.Worker/CLAUDE.md → Unified parent model) -- this is the only MCP-visible signal
|
||||
// that it may have delivered nothing despite that Done status.
|
||||
int RoadblockCount = 0);
|
||||
int RoadblockCount = 0,
|
||||
// The three below are non-null only when Status=Failed; stamped by TaskRunner.MarkFailed via
|
||||
// TaskStateService.FailAsync. failureReason is "unknown" for a Failed task that predates this
|
||||
// field. Lets a caller triage without pulling get_task_log's raw NDJSON.
|
||||
string? FailureReason = null,
|
||||
int? FailureTurnsUsed = null,
|
||||
int? FailureMaxTurns = null);
|
||||
|
||||
// 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.
|
||||
@@ -56,7 +62,10 @@ public sealed record TaskRefDto(
|
||||
string Status,
|
||||
int SortOrder,
|
||||
bool IsMyDay,
|
||||
int RoadblockCount = 0);
|
||||
int RoadblockCount = 0,
|
||||
string? FailureReason = null,
|
||||
int? FailureTurnsUsed = null,
|
||||
int? FailureMaxTurns = null);
|
||||
|
||||
// tasks is populated when includeDescription=false (the default): lean references, no
|
||||
// Description/Result. tasksFull is populated when includeDescription=true: full tasks incl.
|
||||
@@ -236,7 +245,8 @@ public sealed class ExternalMcpService
|
||||
"A successful run lands in WaitingForReview; use review_task to approve, reject or cancel it. " +
|
||||
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution. A Queued task with a blocker waits " +
|
||||
"for its predecessor before the picker will claim it, and WaitingForChildren is a parent whose own work " +
|
||||
"is done but whose children are still running.")]
|
||||
"is done but whose children are still running. For Status=Failed, failureReason (max_turns|timeout|" +
|
||||
"error|cancelled|unknown) plus failureTurnsUsed/failureMaxTurns say why without pulling get_task_log.")]
|
||||
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
@@ -1500,7 +1510,10 @@ public sealed class ExternalMcpService
|
||||
t.FinishedAt,
|
||||
t.IsMyDay,
|
||||
t.SortOrder,
|
||||
t.RoadblockCount);
|
||||
t.RoadblockCount,
|
||||
FailureReasonOf(t),
|
||||
t.Status == TaskStatus.Failed ? t.FailureTurnsUsed : null,
|
||||
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null);
|
||||
|
||||
private static TaskRefDto ToRefDto(TaskEntity t) => new(
|
||||
t.Id,
|
||||
@@ -1509,7 +1522,15 @@ public sealed class ExternalMcpService
|
||||
t.Status.ToString(),
|
||||
t.SortOrder,
|
||||
t.IsMyDay,
|
||||
t.RoadblockCount);
|
||||
t.RoadblockCount,
|
||||
FailureReasonOf(t),
|
||||
t.Status == TaskStatus.Failed ? t.FailureTurnsUsed : null,
|
||||
t.Status == TaskStatus.Failed ? t.FailureMaxTurns : null);
|
||||
|
||||
// "unknown" covers a Failed task that predates this field (never got a classified reason
|
||||
// stamped) — a defined value rather than null so callers don't have to special-case it.
|
||||
private static string? FailureReasonOf(TaskEntity t) =>
|
||||
t.Status == TaskStatus.Failed ? (t.FailureReason ?? "unknown") : null;
|
||||
}
|
||||
|
||||
internal static class DailyPrepFilter
|
||||
|
||||
+6
-1
@@ -20,7 +20,12 @@ public sealed class LifecycleMcpTools
|
||||
_reset = reset;
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Reset a failed task back to Idle so it can be run again, discarding its now-stale worktree. Only tasks with Status=Failed are accepted; other statuses throw.")]
|
||||
[McpServerTool, Description(
|
||||
"Discards a failed task's worktree (and all uncommitted work in it) and resets it to Idle for a fresh " +
|
||||
"run. Only tasks with Status=Failed are accepted; other statuses throw. Check failureReason from " +
|
||||
"get_task/batch_get_tasks first: for failureReason=\"max_turns\" the worktree's work is usually still " +
|
||||
"good and continue_task (resume the session, keep the worktree) is almost always the right call instead " +
|
||||
"— reach for this tool only for a real error, not a task that just ran out of turns.")]
|
||||
public async Task<ResetFailedTaskResult> ResetFailedTask(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, cancellationToken)
|
||||
|
||||
@@ -158,12 +158,14 @@ public sealed class TaskRunner
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, retryResult.ErrorMarkdown, retryResult.TurnCount);
|
||||
await MarkFailed(task.Id, task.Title, slot, retryResult.ErrorMarkdown, retryResult.TurnCount,
|
||||
retryConfig.MaxTurns, ClassifyFailureReason(retryResult.TerminalReason));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, result.ErrorMarkdown, result.TurnCount);
|
||||
await MarkFailed(task.Id, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +174,7 @@ public sealed class TaskRunner
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Task {TaskId} was cancelled", task.Id);
|
||||
await MarkFailed(task.Id, task.Title, slot, "Task cancelled.");
|
||||
await MarkFailed(task.Id, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -270,7 +272,8 @@ public sealed class TaskRunner
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(taskId, task.Title, slot, result.ErrorMarkdown, result.TurnCount);
|
||||
await MarkFailed(taskId, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
}
|
||||
|
||||
await _broadcaster.TaskUpdated(taskId);
|
||||
@@ -278,7 +281,7 @@ public sealed class TaskRunner
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Task {TaskId} was cancelled during continue", taskId);
|
||||
await MarkFailed(taskId, task.Title, slot, "Task cancelled.");
|
||||
await MarkFailed(taskId, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -404,6 +407,9 @@ public sealed class TaskRunner
|
||||
run.ErrorMarkdown = result.ErrorMarkdown;
|
||||
run.ExitCode = result.ExitCode;
|
||||
run.TurnCount = result.TurnCount;
|
||||
run.ResultSubtype = result.ResultSubtype;
|
||||
run.TerminalReason = result.TerminalReason;
|
||||
run.Errors = result.Errors.Count > 0 ? string.Join("\n", result.Errors) : null;
|
||||
if (result.SessionId is not null)
|
||||
await ApplyUsageAsync(run, taskId, result.SessionId);
|
||||
run.FinishedAt = DateTime.UtcNow;
|
||||
@@ -421,6 +427,7 @@ public sealed class TaskRunner
|
||||
// Ensure the run row is completed so ContinueAsync / inspection
|
||||
// isn't left staring at a null session_id / finished_at.
|
||||
run.ErrorMarkdown = "Cancelled.";
|
||||
run.TerminalReason = "cancelled";
|
||||
run.ExitCode = -1;
|
||||
run.FinishedAt = DateTime.UtcNow;
|
||||
try
|
||||
@@ -550,14 +557,17 @@ public sealed class TaskRunner
|
||||
task.Id, result.TurnCount, result.TokensIn, result.TokensOut);
|
||||
}
|
||||
|
||||
private async Task MarkFailed(string taskId, string taskTitle, string slot, string? error, int turnCount = 0)
|
||||
private async Task MarkFailed(
|
||||
string taskId, string taskTitle, string slot, string? error, int turnCount = 0,
|
||||
int? maxTurns = null, string failureReason = "error")
|
||||
{
|
||||
// Terminal write for a failed task: never cancel (the status must always
|
||||
// be persisted) and never throw (a logging failure must not mask the error).
|
||||
try
|
||||
{
|
||||
var finishedAt = DateTime.UtcNow;
|
||||
await _state.FailAsync(taskId, finishedAt, error, CancellationToken.None);
|
||||
await _state.FailAsync(taskId, finishedAt, error, CancellationToken.None,
|
||||
failureReason, turnCount > 0 ? turnCount : null, maxTurns);
|
||||
await _broadcaster.WorkerLog($"Finished \"{taskTitle}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, taskId, "failed", finishedAt);
|
||||
_logger.LogWarning("Task {TaskId} failed (turns={Turns}): {Error}", taskId, turnCount, error);
|
||||
@@ -568,6 +578,16 @@ public sealed class TaskRunner
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Classifies the CLI's raw <c>terminal_reason</c> into the small, MCP-facing enum
|
||||
/// (<c>max_turns|timeout|error</c>) get_task/batch_get_tasks report as failureReason.
|
||||
/// "cancelled" is set explicitly at the call sites that know it (there's no CLI signal for it).</summary>
|
||||
internal static string ClassifyFailureReason(string? terminalReason) => terminalReason switch
|
||||
{
|
||||
"max_turns" => "max_turns",
|
||||
"timeout" => "timeout",
|
||||
_ => "error",
|
||||
};
|
||||
|
||||
private string BuildRunMcpConfigJson(string token)
|
||||
{
|
||||
var payload = new
|
||||
|
||||
@@ -8,7 +8,9 @@ public interface ITaskStateService
|
||||
Task<TransitionResult> SubmitForReviewAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct);
|
||||
Task<TransitionResult> SubmitInteractiveForReviewAsync(string taskId, DateTime finishedAt, CancellationToken ct);
|
||||
Task<TransitionResult> SubmitForChildrenAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct);
|
||||
Task<TransitionResult> FailAsync(string taskId, DateTime finishedAt, string? error, CancellationToken ct);
|
||||
Task<TransitionResult> FailAsync(
|
||||
string taskId, DateTime finishedAt, string? error, CancellationToken ct,
|
||||
string failureReason = "error", int? turnsUsed = null, int? maxTurns = null);
|
||||
Task<TransitionResult> CancelAsync(string taskId, DateTime finishedAt, CancellationToken ct, bool allowFromIdle = false);
|
||||
Task<TransitionResult> ResetToIdleAsync(string taskId, CancellationToken ct);
|
||||
|
||||
|
||||
@@ -226,7 +226,9 @@ public sealed class TaskStateService : ITaskStateService
|
||||
: new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
public async Task<TransitionResult> FailAsync(string taskId, DateTime finishedAt, string? error, CancellationToken ct)
|
||||
public async Task<TransitionResult> FailAsync(
|
||||
string taskId, DateTime finishedAt, string? error, CancellationToken ct,
|
||||
string failureReason = "error", int? turnsUsed = null, int? maxTurns = null)
|
||||
{
|
||||
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
{
|
||||
@@ -239,7 +241,10 @@ public sealed class TaskStateService : ITaskStateService
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, TaskStatus.Failed)
|
||||
.SetProperty(t => t.FinishedAt, finishedAt)
|
||||
.SetProperty(t => t.Result, error), ct);
|
||||
.SetProperty(t => t.Result, error)
|
||||
.SetProperty(t => t.FailureReason, failureReason)
|
||||
.SetProperty(t => t.FailureTurnsUsed, turnsUsed)
|
||||
.SetProperty(t => t.FailureMaxTurns, maxTurns), ct);
|
||||
|
||||
if (affected == 0)
|
||||
return new TransitionResult(false, "Task not in a failable state (must be Running or Queued).");
|
||||
|
||||
Reference in New Issue
Block a user