diff --git a/src/ClaudeDo.Worker/Prime/IPrimeClock.cs b/src/ClaudeDo.Worker/Prime/IPrimeClock.cs new file mode 100644 index 0000000..ec3204e --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/IPrimeClock.cs @@ -0,0 +1,2 @@ +namespace ClaudeDo.Worker.Prime; +public interface IPrimeClock { DateTimeOffset Now { get; } } diff --git a/src/ClaudeDo.Worker/Prime/IPrimeRunner.cs b/src/ClaudeDo.Worker/Prime/IPrimeRunner.cs new file mode 100644 index 0000000..2efb578 --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/IPrimeRunner.cs @@ -0,0 +1,8 @@ +namespace ClaudeDo.Worker.Prime; + +public interface IPrimeRunner +{ + Task FireAsync(PrimeScheduleDto schedule, CancellationToken ct); +} + +public sealed record PrimeRunOutcome(bool Success, string Message); diff --git a/src/ClaudeDo.Worker/Prime/IPrimeScheduleSignal.cs b/src/ClaudeDo.Worker/Prime/IPrimeScheduleSignal.cs new file mode 100644 index 0000000..c633eb9 --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/IPrimeScheduleSignal.cs @@ -0,0 +1,6 @@ +namespace ClaudeDo.Worker.Prime; +public interface IPrimeScheduleSignal +{ + void Signal(); + CancellationToken CurrentToken { get; } +} diff --git a/src/ClaudeDo.Worker/Prime/PrimeClock.cs b/src/ClaudeDo.Worker/Prime/PrimeClock.cs new file mode 100644 index 0000000..9ce107e --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/PrimeClock.cs @@ -0,0 +1,5 @@ +namespace ClaudeDo.Worker.Prime; +public sealed class PrimeClock : IPrimeClock +{ + public DateTimeOffset Now => DateTimeOffset.Now; +} diff --git a/src/ClaudeDo.Worker/Prime/PrimeRunner.cs b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs new file mode 100644 index 0000000..0c88eeb --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs @@ -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 _logger; + + public PrimeRunner(IClaudeProcess claude, ILogger logger) + { + _claude = claude; + _logger = logger; + } + + public async Task 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}"; +} diff --git a/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs b/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs new file mode 100644 index 0000000..f7ab7e7 --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/PrimeScheduleSignal.cs @@ -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(); + } +} diff --git a/src/ClaudeDo.Worker/Prime/PrimeSchedulerOptions.cs b/src/ClaudeDo.Worker/Prime/PrimeSchedulerOptions.cs new file mode 100644 index 0000000..a6d8a1b --- /dev/null +++ b/src/ClaudeDo.Worker/Prime/PrimeSchedulerOptions.cs @@ -0,0 +1,7 @@ +namespace ClaudeDo.Worker.Prime; + +public sealed record PrimeSchedulerOptions(TimeSpan CatchUpWindow) +{ + public static PrimeSchedulerOptions Default { get; } = + new(TimeSpan.FromMinutes(30)); +}