feat(daily-prep): persist last prep run to a log file and serve it via GetLastPrepLog

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 09:39:11 +02:00
parent 3a40e39fc8
commit 4d82079cac
4 changed files with 48 additions and 1 deletions

View File

@@ -582,6 +582,19 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
await new DailyNoteRepository(ctx).DeleteAsync(id);
}
public Task<string> GetLastPrepLog()
{
var path = DailyPrepPrompt.LogPath();
if (!File.Exists(path)) return Task.FromResult(string.Empty);
const int maxBytes = 256 * 1024;
var bytes = File.ReadAllBytes(path);
var text = bytes.Length <= maxBytes
? System.Text.Encoding.UTF8.GetString(bytes)
: System.Text.Encoding.UTF8.GetString(bytes, bytes.Length - maxBytes, maxBytes);
return Task.FromResult(text);
}
public async Task<int> ClearMyDay()
{
await using var ctx = await _dbFactory.CreateDbContextAsync();

View File

@@ -5,6 +5,9 @@ public static class DailyPrepPrompt
public const string CandidatesTool = "mcp__claudedo__get_daily_prep_candidates";
public const string SetMyDayTool = "mcp__claudedo__set_my_day";
public static string LogPath() =>
System.IO.Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "logs", "daily-prep.log");
public static string BuildArgs(int maxTurns) =>
"-p --output-format stream-json --verbose --permission-mode acceptEdits " +
$"--max-turns {maxTurns} " +

View File

@@ -39,6 +39,10 @@ public sealed class PrimeRunner : IPrimeRunner
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();
@@ -62,7 +66,11 @@ public sealed class PrimeRunner : IPrimeRunner
arguments: args,
prompt: prompt,
workingDirectory: cwd,
onStdoutLine: line => _broadcaster.PrepLineAsync(line),
onStdoutLine: async line =>
{
await logWriter.WriteLineAsync(line);
await _broadcaster.PrepLineAsync(line);
},
ct: timeoutCts.Token);
success = result.IsSuccess;

View File

@@ -124,4 +124,27 @@ public class PrimeRunnerTests : IDisposable
Assert.Single(broadcaster.FinishedResults);
Assert.True(broadcaster.FinishedResults[0]);
}
[Fact]
public async Task FireAsync_writes_last_run_to_prep_log_file()
{
var path = DailyPrepPrompt.LogPath();
if (File.Exists(path)) File.Delete(path);
var claude = new FakeClaudeProcess(emitLines: new[] { "lineA", "lineB" }, exitCode: 0, result: "ok");
var runner = NewRunner(claude, new RecordingPrimeBroadcaster());
await runner.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
var contents = await File.ReadAllTextAsync(path);
Assert.Contains("lineA", contents);
Assert.Contains("lineB", contents);
// Truncation: a second run with different lines replaces the file.
var claude2 = new FakeClaudeProcess(emitLines: new[] { "lineC" }, exitCode: 0, result: "ok");
var runner2 = NewRunner(claude2, new RecordingPrimeBroadcaster());
await runner2.FireAsync(new PrimeScheduleDto(Guid.Empty, 0, TimeSpan.Zero, true, null, null), CancellationToken.None);
var after = await File.ReadAllTextAsync(path);
Assert.DoesNotContain("lineA", after);
Assert.Contains("lineC", after);
}
}