97 lines
3.3 KiB
C#
97 lines
3.3 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Runner;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Worker.Prime;
|
|
|
|
public sealed class PrimeRunner : IPrimeRunner
|
|
{
|
|
private static readonly TimeSpan FireTimeout = TimeSpan.FromMinutes(5);
|
|
private const int MaxTurns = 30;
|
|
|
|
private readonly IClaudeProcess _claude;
|
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
private readonly IPrimeClock _clock;
|
|
private readonly ILogger<PrimeRunner> _logger;
|
|
private readonly IPrimeBroadcaster _broadcaster;
|
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
|
|
|
public PrimeRunner(
|
|
IClaudeProcess claude,
|
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
|
IPrimeClock clock,
|
|
ILogger<PrimeRunner> logger,
|
|
IPrimeBroadcaster broadcaster)
|
|
{
|
|
_claude = claude;
|
|
_dbFactory = dbFactory;
|
|
_clock = clock;
|
|
_logger = logger;
|
|
_broadcaster = broadcaster;
|
|
}
|
|
|
|
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
|
{
|
|
if (!await _gate.WaitAsync(0, ct))
|
|
return new PrimeRunOutcome(false, "Daily prep already running");
|
|
|
|
var success = false;
|
|
try
|
|
{
|
|
var logPath = DailyPrepPrompt.LogPath();
|
|
try { if (File.Exists(logPath)) File.Delete(logPath); } catch { /* best effort */ }
|
|
await using var logWriter = new LogWriter(logPath);
|
|
|
|
await _broadcaster.PrepStartedAsync();
|
|
|
|
var cwd = Paths.AppDataRoot();
|
|
Directory.CreateDirectory(cwd);
|
|
|
|
int maxTasks;
|
|
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
|
{
|
|
var settings = await new AppSettingsRepository(dbCtx).GetAsync(ct);
|
|
maxTasks = settings.DailyPrepMaxTasks < 1 ? 1 : settings.DailyPrepMaxTasks;
|
|
}
|
|
|
|
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
|
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
|
|
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
|
timeoutCts.CancelAfter(FireTimeout);
|
|
|
|
var result = await _claude.RunAsync(
|
|
arguments: args,
|
|
prompt: prompt,
|
|
workingDirectory: cwd,
|
|
onStdoutLine: async line =>
|
|
{
|
|
await logWriter.WriteLineAsync(line);
|
|
await _broadcaster.PrepLineAsync(line);
|
|
},
|
|
ct: timeoutCts.Token);
|
|
|
|
success = result.IsSuccess;
|
|
return success
|
|
? new PrimeRunOutcome(true, "Daily prep complete")
|
|
: new PrimeRunOutcome(false, $"exit code {result.ExitCode}");
|
|
}
|
|
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
|
{
|
|
return new PrimeRunOutcome(false, $"timed out after {FireTimeout.TotalMinutes:0} min");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Daily prep run failed");
|
|
return new PrimeRunOutcome(false, ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
await _broadcaster.PrepFinishedAsync(success);
|
|
_gate.Release();
|
|
}
|
|
}
|
|
}
|