feat(prime): branch PrimeRunner on the schedule action kind
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
/// <summary>Prompts and CLI args for the non-daily-prep Prime actions.
|
||||
/// The daily-prep ones live in <see cref="DailyPrepPrompt"/>.</summary>
|
||||
public static class PrimePrompts
|
||||
{
|
||||
/// <summary>The whole point of a ping is to open the usage window, so it asks for the
|
||||
/// cheapest possible answer. Not user-editable — a schedule's prompt override is ignored
|
||||
/// for this kind.</summary>
|
||||
public const string PingPrompt = "Reply with the single word: ok";
|
||||
|
||||
/// <summary>No tools at all: --strict-mcp-config without a --mcp-config argument loads no
|
||||
/// MCP server, and one turn leaves no room to act on a tool result anyway.</summary>
|
||||
public static IReadOnlyList<string> BuildPingArgs() =>
|
||||
[
|
||||
"-p", "--output-format", "stream-json", "--verbose",
|
||||
"--permission-mode", "default",
|
||||
"--max-turns", "1",
|
||||
"--strict-mcp-config",
|
||||
];
|
||||
|
||||
/// <summary>ClaudeDo's MCP server only — no Read/Write/Edit/Bash. An unattended 07:00 run
|
||||
/// may create and queue tasks; it may not touch the filesystem.</summary>
|
||||
public static IReadOnlyList<string> BuildCustomArgs(int maxTurns) =>
|
||||
[
|
||||
"-p", "--output-format", "stream-json", "--verbose",
|
||||
"--permission-mode", "acceptEdits",
|
||||
"--max-turns", maxTurns.ToString(),
|
||||
"--allowedTools", "mcp__claudedo",
|
||||
];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -33,9 +34,27 @@ public sealed class PrimeRunner : IPrimeRunner
|
||||
|
||||
public async Task<PrimeRunOutcome> FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||
{
|
||||
// One gate for all kinds — two schedules must never run at the same time.
|
||||
if (!await _gate.WaitAsync(0, ct))
|
||||
return new PrimeRunOutcome(false, "Daily prep already running");
|
||||
return new PrimeRunOutcome(false, "Prime run already running");
|
||||
|
||||
try
|
||||
{
|
||||
return schedule.Kind switch
|
||||
{
|
||||
PrimeActionKind.FillMyDay => await RunFillMyDayAsync(schedule, ct),
|
||||
PrimeActionKind.Custom => await RunCustomAsync(schedule, ct),
|
||||
_ => await RunPingAsync(ct),
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PrimeRunOutcome> RunFillMyDayAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||
{
|
||||
var success = false;
|
||||
try
|
||||
{
|
||||
@@ -45,8 +64,7 @@ public sealed class PrimeRunner : IPrimeRunner
|
||||
|
||||
await _broadcaster.PrepStartedAsync();
|
||||
|
||||
var cwd = Paths.AppDataRoot();
|
||||
Directory.CreateDirectory(cwd);
|
||||
var cwd = Cwd();
|
||||
|
||||
int maxTasks;
|
||||
await using (var dbCtx = await _dbFactory.CreateDbContextAsync(ct))
|
||||
@@ -57,13 +75,12 @@ public sealed class PrimeRunner : IPrimeRunner
|
||||
|
||||
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today, schedule.PromptOverride);
|
||||
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(FireTimeout);
|
||||
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args,
|
||||
arguments: DailyPrepPrompt.BuildArgs(MaxTurns),
|
||||
prompt: prompt,
|
||||
workingDirectory: cwd,
|
||||
onStdoutLine: async line =>
|
||||
@@ -90,7 +107,57 @@ public sealed class PrimeRunner : IPrimeRunner
|
||||
finally
|
||||
{
|
||||
await _broadcaster.PrepFinishedAsync(success);
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private Task<PrimeRunOutcome> RunPingAsync(CancellationToken ct) =>
|
||||
RunQuietAsync(PrimePrompts.BuildPingArgs(), PrimePrompts.PingPrompt, "Ping complete", "Ping", ct);
|
||||
|
||||
private Task<PrimeRunOutcome> RunCustomAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||
{
|
||||
var prompt = schedule.PromptOverride;
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
return Task.FromResult(new PrimeRunOutcome(false, "Custom schedule has no prompt"));
|
||||
|
||||
return RunQuietAsync(PrimePrompts.BuildCustomArgs(MaxTurns), prompt, "Custom run complete", "Custom", ct);
|
||||
}
|
||||
|
||||
/// <summary>Ping and Custom deliberately write no log file and raise no Prep* events — the
|
||||
/// prep log belongs to the MyDay selection and would otherwise be overwritten with noise.</summary>
|
||||
private async Task<PrimeRunOutcome> RunQuietAsync(
|
||||
IReadOnlyList<string> args, string prompt, string successMessage, string label, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
timeoutCts.CancelAfter(FireTimeout);
|
||||
|
||||
var result = await _claude.RunAsync(
|
||||
arguments: args,
|
||||
prompt: prompt,
|
||||
workingDirectory: Cwd(),
|
||||
onStdoutLine: _ => Task.CompletedTask,
|
||||
ct: timeoutCts.Token);
|
||||
|
||||
return result.IsSuccess
|
||||
? new PrimeRunOutcome(true, successMessage)
|
||||
: 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, "Prime {Label} run failed", label);
|
||||
return new PrimeRunOutcome(false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Cwd()
|
||||
{
|
||||
var cwd = Paths.AppDataRoot();
|
||||
Directory.CreateDirectory(cwd);
|
||||
return cwd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public sealed record PrimeScheduleDto(
|
||||
@@ -6,4 +8,5 @@ public sealed record PrimeScheduleDto(
|
||||
TimeSpan TimeOfDay,
|
||||
bool Enabled,
|
||||
DateTimeOffset? LastRunAt,
|
||||
string? PromptOverride);
|
||||
string? PromptOverride,
|
||||
PrimeActionKind Kind);
|
||||
|
||||
@@ -102,7 +102,7 @@ public sealed class PrimeScheduler : BackgroundService
|
||||
}
|
||||
|
||||
private static PrimeScheduleDto ToDto(Data.Models.PrimeScheduleEntity e) =>
|
||||
new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride);
|
||||
new(e.Id, (int)e.Days, e.TimeOfDay, e.Enabled, e.LastRunAt, e.PromptOverride, e.Kind);
|
||||
|
||||
private async Task FireAsync(PrimeScheduleDto schedule, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user