feat(worker): report worktree-creation phase on OperationProgress

Fills the silent gap between Queued and the first agent output: WorktreeManager
now broadcasts a "creating_worktree" phase (before the initial git worktree add
and again for the self-heal retry section) through the existing OperationProgress
channel, and the task row shows it via the pre-existing but unused
ops.worker.creatingWorktree locale key until the next entity refresh clears it.

Confirmed the 2026-08-07 triage finding still holds: TaskRunner already
broadcasts WorktreeUpdated right after WorktreeManager.CreateAsync
(TaskRunner.cs:342-346, :501) -- no second broadcast added there.
This commit is contained in:
mika kuns
2026-08-21 13:35:26 +02:00
parent dcda067b48
commit eba0d842b5
6 changed files with 156 additions and 1 deletions
+18 -1
View File
@@ -4,6 +4,7 @@ using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Hub;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Worker.Runner;
@@ -12,19 +13,31 @@ public sealed record WorktreeContext(string WorktreePath, string BranchName, str
public sealed class WorktreeManager
{
// Phase token for the OperationProgress broadcast while `git worktree add` (+ the
// self-heal retry) is in flight -- stable identifier, localized by the UI via the
// pre-existing "ops.worker.creatingWorktree" key. opKey is the task id.
public const string PhaseCreatingWorktree = "creating_worktree";
private readonly GitService _git;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly WorkerConfig _cfg;
private readonly ILogger<WorktreeManager> _logger;
private readonly HubBroadcaster? _broadcaster;
public WorktreeManager(GitService git, IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerConfig cfg, ILogger<WorktreeManager> logger)
public WorktreeManager(
GitService git, IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerConfig cfg,
ILogger<WorktreeManager> logger, HubBroadcaster? broadcaster = null)
{
_git = git;
_dbFactory = dbFactory;
_cfg = cfg;
_logger = logger;
_broadcaster = broadcaster;
}
private Task BroadcastCreating(string taskId) =>
_broadcaster?.OperationProgress(taskId, PhaseCreatingWorktree, 0, 0) ?? Task.CompletedTask;
public async Task<WorktreeContext> CreateAsync(TaskEntity task, ListEntity list, CancellationToken ct)
{
var workingDir = list.WorkingDir
@@ -63,6 +76,7 @@ public sealed class WorktreeManager
// Create the worktree. If a stale branch from a previous run remains
// (e.g. after force-remove), delete it and retry once.
await BroadcastCreating(task.Id);
try
{
await _git.WorktreeAddAsync(workingDir, branchName, worktreePath, baseCommit, ct);
@@ -70,6 +84,9 @@ public sealed class WorktreeManager
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning("Branch {Branch} already exists; cleaning phantom worktrees and retrying", branchName);
// Self-heal is the slow section (list -> remove -> prune -> branch delete -> retry);
// report it as its own section rather than staying silent through the retry.
await BroadcastCreating(task.Id);
// Find and forcefully remove any existing worktree registered against this branch.
List<string> stalePaths;