refactor(worker): drop ForceSetStatusAsync, route every status write through a guarded transition
Mark-done now fires OnChildTerminalAsync, so ticking a chain child or the last open child off by hand unblocks the successor and advances the parent. The hub's Mark-as menu and update_task_status map onto MarkDone/Cancel(retire)/Enqueue/Reset; revert_merge uses the new ReopenForReviewAsync (Done|WaitingForReview -> WaitingForReview).
This commit is contained in:
@@ -297,13 +297,13 @@ public sealed class TaskStateService : ITaskStateService
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
// allowFromIdle: only the external update_task_status(Cancelled) path sets this — it lets
|
||||
// an Idle task be retired without deleting it. Every other caller (hub CancelReview,
|
||||
// PlanningChainCoordinator's chain-walk, batch cancel) relies on Idle staying a no-op here;
|
||||
// PlanningChainCoordinator specifically uses "parked back to Idle" as a deliberate opt-out
|
||||
// signal, so do not flip this default.
|
||||
// retire: only the user-facing "mark cancelled" paths (update_task_status, the row's
|
||||
// "Mark as" menu) set this — it lets an Idle or Failed task be retired without deleting it.
|
||||
// Every other caller (hub CancelReview, PlanningChainCoordinator's chain-walk, batch cancel)
|
||||
// relies on Idle staying a no-op here; PlanningChainCoordinator specifically uses "parked
|
||||
// back to Idle" as a deliberate opt-out signal, so do not flip this default.
|
||||
public async Task<TransitionResult> CancelAsync(
|
||||
string taskId, DateTime finishedAt, CancellationToken ct, bool allowFromIdle = false)
|
||||
string taskId, DateTime finishedAt, CancellationToken ct, bool retire = false)
|
||||
{
|
||||
// A unit merge drains its children's worktrees onto the target branch while the parent
|
||||
// sits in WaitingForReview for the whole (potentially minutes-long) drain. Cancelling out
|
||||
@@ -321,7 +321,7 @@ public sealed class TaskStateService : ITaskStateService
|
||||
(t.Status == TaskStatus.Running || t.Status == TaskStatus.Queued
|
||||
|| t.Status == TaskStatus.WaitingForReview
|
||||
|| t.Status == TaskStatus.WaitingForChildren
|
||||
|| (allowFromIdle && t.Status == TaskStatus.Idle)))
|
||||
|| (retire && (t.Status == TaskStatus.Idle || t.Status == TaskStatus.Failed))))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, TaskStatus.Cancelled)
|
||||
.SetProperty(t => t.FinishedAt, finishedAt), ct);
|
||||
@@ -381,49 +381,42 @@ public sealed class TaskStateService : ITaskStateService
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
// Unconditional status write — bypasses transition rules. Used by the UI's
|
||||
// "set status freely" affordance; intentionally no guards (caller may strand
|
||||
// the runner if used while a task is executing). It also bypasses chain/parent
|
||||
// advancement — forcing a chain child or a WaitingForChildren parent's child to a
|
||||
// terminal status here does not unblock its successor or re-check the parent.
|
||||
public async Task<TransitionResult> ForceSetStatusAsync(string taskId, TaskStatus status, CancellationToken ct)
|
||||
/// <summary>Done/WaitingForReview → WaitingForReview after a reverted merge, so the task can be re-approved.</summary>
|
||||
public async Task<TransitionResult> ReopenForReviewAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var affected = status == TaskStatus.Done
|
||||
? await ctx.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, status)
|
||||
.SetProperty(t => t.FinishedAt, DateTime.UtcNow), ct)
|
||||
: await ctx.Tasks
|
||||
.Where(t => t.Id == taskId)
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, status), ct);
|
||||
var affected = await ctx.Tasks
|
||||
.Where(t => t.Id == taskId &&
|
||||
(t.Status == TaskStatus.Done || t.Status == TaskStatus.WaitingForReview))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(t => t.Status, TaskStatus.WaitingForReview), ct);
|
||||
|
||||
if (affected == 0)
|
||||
return new TransitionResult(false, "Task not found.");
|
||||
return new TransitionResult(false, "Task is not Done or waiting for review; cannot reopen.");
|
||||
|
||||
if (status == TaskStatus.Queued) _waker.Wake();
|
||||
await NotifyAsync(taskId);
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
public async Task<TransitionResult> MarkDoneAsync(string taskId, DateTime finishedAt, CancellationToken ct)
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
// The done toggle is a manual "tick it off" affordance and works from every status
|
||||
// except Running (a live run keeps writing the row, so the picker/runner would race).
|
||||
// WaitingForReview is deliberately included: ticking a finished task done skips the
|
||||
// merge on purpose -- for a worktree-less list-handler task there is nothing to merge,
|
||||
// and for a worktree task the branch just stays Active for the worktrees overview.
|
||||
var affected = await ctx.Tasks
|
||||
.Where(t => t.Id == taskId && t.Status != TaskStatus.Running)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, TaskStatus.Done)
|
||||
.SetProperty(t => t.FinishedAt, finishedAt), ct);
|
||||
await using (var ctx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
{
|
||||
// The done toggle is a manual "tick it off" affordance and works from every status
|
||||
// except Running (a live run keeps writing the row, so the picker/runner would race).
|
||||
// WaitingForReview is deliberately included: ticking a finished task done skips the
|
||||
// merge on purpose -- for a worktree-less list-handler task there is nothing to merge,
|
||||
// and for a worktree task the branch just stays Active for the worktrees overview.
|
||||
var affected = await ctx.Tasks
|
||||
.Where(t => t.Id == taskId && t.Status != TaskStatus.Running)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(t => t.Status, TaskStatus.Done)
|
||||
.SetProperty(t => t.FinishedAt, finishedAt), ct);
|
||||
|
||||
if (affected == 0)
|
||||
return new TransitionResult(false, "Task is running or no longer exists; cannot mark done.");
|
||||
if (affected == 0)
|
||||
return new TransitionResult(false, "Task is running or no longer exists; cannot mark done.");
|
||||
}
|
||||
|
||||
await OnChildTerminalAsync(taskId, TaskStatus.Done);
|
||||
await NotifyAsync(taskId);
|
||||
return new TransitionResult(true, null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user