From ea4d2d7c0c4f11356f073a5fe657f64a6067bd2e Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 23 Apr 2026 14:46:10 +0200 Subject: [PATCH] feat(worker): emit WorkerLog events from TaskRunner --- src/ClaudeDo.Worker/Runner/TaskRunner.cs | 34 +++++++++++++++--------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index ce1246d..0be91e4 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -49,7 +49,7 @@ public sealed class TaskRunner list = await listRepo.GetByIdAsync(task.ListId, ct); if (list is null) { - await MarkFailed(task.Id, slot, "List not found."); + await MarkFailed(task.Id, task.Title, slot, "List not found."); return; } listConfig = await listRepo.GetConfigAsync(task.ListId, ct); @@ -67,12 +67,13 @@ public sealed class TaskRunner try { wtCtx = await _wtManager.CreateAsync(task, list, ct); + await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); runDir = wtCtx.WorktreePath; } catch (Exception ex) { _logger.LogError(ex, "Failed to create worktree for task {TaskId}", task.Id); - await MarkFailed(task.Id, slot, $"Worktree creation failed: {ex.Message}"); + await MarkFailed(task.Id, task.Title, slot, $"Worktree creation failed: {ex.Message}"); return; } } @@ -104,7 +105,7 @@ public sealed class TaskRunner var prompt = sb.ToString(); // Run 1. - var result = await RunOnceAsync(task.Id, slot, runDir, resolvedConfig, 1, false, prompt, ct); + var result = await RunOnceAsync(task.Id, task.Title, slot, runDir, resolvedConfig, 1, false, prompt, ct); if (result.IsSuccess) { @@ -119,7 +120,7 @@ public sealed class TaskRunner var retryConfig = resolvedConfig with { ResumeSessionId = result.SessionId }; var retryPrompt = $"The previous attempt failed with:\n\n{result.ErrorMarkdown}\n\nTry again and fix the issues."; - var retryResult = await RunOnceAsync(task.Id, slot, runDir, retryConfig, 2, true, retryPrompt, ct); + var retryResult = await RunOnceAsync(task.Id, task.Title, slot, runDir, retryConfig, 2, true, retryPrompt, ct); if (retryResult.IsSuccess) { @@ -127,12 +128,12 @@ public sealed class TaskRunner } else { - await HandleFailure(task.Id, slot, retryResult); + await HandleFailure(task.Id, task.Title, slot, retryResult); } } else { - await HandleFailure(task.Id, slot, result); + await HandleFailure(task.Id, task.Title, slot, result); } } @@ -141,12 +142,12 @@ public sealed class TaskRunner catch (OperationCanceledException) { _logger.LogInformation("Task {TaskId} was cancelled", task.Id); - await MarkFailed(task.Id, slot, "Task cancelled."); + await MarkFailed(task.Id, task.Title, slot, "Task cancelled."); } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception running task {TaskId}", task.Id); - await MarkFailed(task.Id, slot, $"Unhandled error: {ex.Message}"); + await MarkFailed(task.Id, task.Title, slot, $"Unhandled error: {ex.Message}"); } } @@ -204,7 +205,7 @@ public sealed class TaskRunner await _broadcaster.TaskStarted(slot, taskId, now); var nextRunNumber = lastRun.RunNumber + 1; - var result = await RunOnceAsync(taskId, slot, runDir, resolvedConfig, nextRunNumber, false, followUpPrompt, ct); + var result = await RunOnceAsync(taskId, task.Title, slot, runDir, resolvedConfig, nextRunNumber, false, followUpPrompt, ct); if (result.IsSuccess) { @@ -212,14 +213,14 @@ public sealed class TaskRunner } else { - await HandleFailure(taskId, slot, result); + await HandleFailure(taskId, task.Title, slot, result); } await _broadcaster.TaskUpdated(taskId); } private async Task RunOnceAsync( - string taskId, string slot, string runDir, ClaudeRunConfig config, + string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config, int runNumber, bool isRetry, string prompt, CancellationToken ct) { var runId = Guid.NewGuid().ToString(); @@ -250,6 +251,7 @@ public sealed class TaskRunner try { + await _broadcaster.WorkerLog($"Started Claude for \"{taskTitle}\"", WorkerLogLevel.Info, DateTime.UtcNow); var result = await _claude.RunAsync( arguments, prompt, @@ -315,7 +317,10 @@ public sealed class TaskRunner { var committed = await _wtManager.CommitIfChangedAsync(wtCtx, task, list, ct); if (committed) + { + await _broadcaster.WorkerLog($"Committed changes in \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow); await _broadcaster.WorktreeUpdated(task.Id); + } } // Terminal DB write uses CancellationToken.None so the task status @@ -327,12 +332,13 @@ public sealed class TaskRunner var taskRepo = new TaskRepository(context); await taskRepo.MarkDoneAsync(task.Id, finishedAt, result.ResultMarkdown, CancellationToken.None); } + await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt); _logger.LogInformation("Task {TaskId} completed (turns={Turns}, tokens_in={In}, tokens_out={Out})", task.Id, result.TurnCount, result.TokensIn, result.TokensOut); } - private async Task HandleFailure(string taskId, string slot, RunResult result) + private async Task HandleFailure(string taskId, string taskTitle, string slot, RunResult result) { // Intentionally does not accept a CancellationToken: this is the // terminal write for a failed task and must always be persisted. @@ -340,11 +346,12 @@ public sealed class TaskRunner using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); await taskRepo.MarkFailedAsync(taskId, finishedAt, result.ErrorMarkdown, CancellationToken.None); + 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, result.TurnCount, result.ErrorMarkdown); } - private async Task MarkFailed(string taskId, string slot, string error) + private async Task MarkFailed(string taskId, string taskTitle, string slot, string error) { try { @@ -353,6 +360,7 @@ public sealed class TaskRunner using var context = _dbFactory.CreateDbContext(); var taskRepo = new TaskRepository(context); await taskRepo.MarkFailedAsync(taskId, now, error, CancellationToken.None); + await _broadcaster.WorkerLog($"Finished \"{taskTitle}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow); await _broadcaster.TaskFinished(slot, taskId, "failed", now); await _broadcaster.TaskUpdated(taskId); }