From 4d82079cacdb890ea3e90c084c4dd6ce5c8b269b Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 09:39:11 +0200 Subject: [PATCH] feat(daily-prep): persist last prep run to a log file and serve it via GetLastPrepLog Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Worker/Hub/WorkerHub.cs | 13 +++++++++++ src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs | 3 +++ src/ClaudeDo.Worker/Prime/PrimeRunner.cs | 10 +++++++- .../Prime/PrimeRunnerTests.cs | 23 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index 415ef19..af1fad9 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -582,6 +582,19 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub await new DailyNoteRepository(ctx).DeleteAsync(id); } + public Task 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 ClearMyDay() { await using var ctx = await _dbFactory.CreateDbContextAsync(); diff --git a/src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs b/src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs index b0595e4..7e880bd 100644 --- a/src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs +++ b/src/ClaudeDo.Worker/Prime/DailyPrepPrompt.cs @@ -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} " + diff --git a/src/ClaudeDo.Worker/Prime/PrimeRunner.cs b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs index 0577716..f7b7790 100644 --- a/src/ClaudeDo.Worker/Prime/PrimeRunner.cs +++ b/src/ClaudeDo.Worker/Prime/PrimeRunner.cs @@ -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; diff --git a/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs b/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs index d2af495..be486df 100644 --- a/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Prime/PrimeRunnerTests.cs @@ -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); + } }