feat(worker): add claude-cli runner, queue service, and hub api
Runner stack (non-worktree path): IClaudeProcess + ClaudeProcess spawning the CLI with --output-format stream-json, prompt via stdin, parses the final type:"result" line into RunResult. LogWriter appends ndjson to ~/.todo-app/logs/<taskId>.ndjson. TaskRunner orchestrates DB transitions (MarkRunning -> MarkDone/Failed) and pushes TaskStarted/Message/Finished/ Updated via HubBroadcaster. Worktree-backed lists short-circuit with a "Slice E" failure message until git support lands. QueueService (BackgroundService) holds two in-memory slots (_queueSlot + _overrideSlot) guarded by a lock. Uses PeriodicTimer + SemaphoreSlim wake signal so WakeQueue() triggers an instant pickup. RunNow throws InvalidOperationException when override busy; CancelTask cancels the linked CTS which kills the child process tree. WorkerHub extended with GetActive, RunNow (translated to HubException variants), CancelTask, WakeQueue. HubBroadcaster exposes typed push methods. Tests: 26 pass (12 new). QueueServiceTests cover override-busy, schedule-filter, FIFO sequentiality, cancellation, plus a FakeClaudeProcess that blocks on a TCS for deterministic slot-state assertions. MessageParserTests cover result extraction + malformed/non-result lines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,44 @@
|
||||
using System.Reflection;
|
||||
using ClaudeDo.Worker.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ClaudeDo.Worker.Hub;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub the UI connects to. Only <see cref="Ping"/> is implemented at this stage;
|
||||
/// RunNow/CancelTask/WakeQueue/GetActive land here once QueueService exists.
|
||||
/// </summary>
|
||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
{
|
||||
private static readonly string Version =
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||
|
||||
private readonly QueueService _queue;
|
||||
|
||||
public WorkerHub(QueueService queue) => _queue = queue;
|
||||
|
||||
public string Ping() => $"pong v{Version}";
|
||||
|
||||
public IReadOnlyList<object> GetActive()
|
||||
{
|
||||
return _queue.GetActive()
|
||||
.Select(a => (object)new { slot = a.slot, taskId = a.taskId, startedAt = a.startedAt })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task RunNow(string taskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _queue.RunNow(taskId);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new HubException("override slot busy");
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
throw new HubException("task not found");
|
||||
}
|
||||
}
|
||||
|
||||
public bool CancelTask(string taskId) => _queue.CancelTask(taskId);
|
||||
|
||||
public void WakeQueue() => _queue.WakeQueue();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user