feat(worker): add Prime scheduler abstractions + runner

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-28 08:57:02 +02:00
parent 4e90828653
commit f383645360
7 changed files with 110 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
namespace ClaudeDo.Worker.Prime;
public interface IPrimeClock { DateTimeOffset Now { get; } }

View File

@@ -0,0 +1,8 @@
namespace ClaudeDo.Worker.Prime;
public interface IPrimeRunner
{
Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct);
}
public sealed record PrimeRunOutcome(bool Success, string Message);

View File

@@ -0,0 +1,6 @@
namespace ClaudeDo.Worker.Prime;
public interface IPrimeScheduleSignal
{
void Signal();
CancellationToken CurrentToken { get; }
}

View File

@@ -0,0 +1,5 @@
namespace ClaudeDo.Worker.Prime;
public sealed class PrimeClock : IPrimeClock
{
public DateTimeOffset Now => DateTimeOffset.Now;
}

View File

@@ -0,0 +1,53 @@
using ClaudeDo.Data;
using ClaudeDo.Worker.Runner;
namespace ClaudeDo.Worker.Prime;
public sealed class PrimeRunner : IPrimeRunner
{
private static readonly TimeSpan FireTimeout = TimeSpan.FromSeconds(60);
private readonly IClaudeProcess _claude;
private readonly ILogger<PrimeRunner> _logger;
public PrimeRunner(IClaudeProcess claude, ILogger<PrimeRunner> logger)
{
_claude = claude;
_logger = logger;
}
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
{
var cwd = Paths.AppDataRoot();
Directory.CreateDirectory(cwd);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(FireTimeout);
try
{
var prompt = schedule.PromptOverride ?? "ping";
var result = await _claude.RunAsync(
arguments: "-p --max-turns 1",
prompt: prompt,
workingDirectory: cwd,
onStdoutLine: _ => Task.CompletedTask,
ct: timeoutCts.Token);
if (IsSuccess(result))
return new PrimeRunOutcome(true, "Primed Claude");
return new PrimeRunOutcome(false, FailureMessage(result));
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !ct.IsCancellationRequested)
{
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalSeconds:0}s");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Prime fire failed");
return new PrimeRunOutcome(false, ex.Message);
}
}
private static bool IsSuccess(RunResult result) => result.IsSuccess;
private static string FailureMessage(RunResult result) => $"exit code {result.ExitCode}";
}

View File

@@ -0,0 +1,29 @@
namespace ClaudeDo.Worker.Prime;
public sealed class PrimeScheduleSignal : IPrimeScheduleSignal, IDisposable
{
private CancellationTokenSource _cts = new();
private readonly object _lock = new();
public CancellationToken CurrentToken
{
get { lock (_lock) return _cts.Token; }
}
public void Signal()
{
CancellationTokenSource old;
lock (_lock)
{
old = _cts;
_cts = new CancellationTokenSource();
}
try { old.Cancel(); } catch { /* already cancelled */ }
old.Dispose();
}
public void Dispose()
{
lock (_lock) _cts.Dispose();
}
}

View File

@@ -0,0 +1,7 @@
namespace ClaudeDo.Worker.Prime;
public sealed record PrimeSchedulerOptions(TimeSpan CatchUpWindow)
{
public static PrimeSchedulerOptions Default { get; } =
new(TimeSpan.FromMinutes(30));
}