feat: Scheduled tasks (cron jobs) for recurring prompt execution#380
feat: Scheduled tasks (cron jobs) for recurring prompt execution#380
Conversation
109fbaf to
01825de
Compare
🤖 Multi-Model Code Review — PR #380feat: Scheduled tasks (cron jobs) for recurring prompt execution 🔴 CRITICAL — Scheduled task service never starts unless the page is visitedFile:
Impact: The core feature (background recurring execution) silently does nothing for users who don't visit the page after every app launch. Fix: Eagerly resolve the service after var app = builder.Build();
app.Services.GetRequiredService<ScheduledTaskService>();
return app;🔴 CRITICAL — Cron tasks never fire (off-by-one in time calculation)File: var candidate = new DateTime(local.Year, local.Month, local.Day,
local.Hour, local.Minute, 0).AddMinutes(1);The candidate always starts 1 minute in the future. Impact: Cron-type schedules never auto-execute. Fix: Start from the current minute rather than next minute: var candidate = new DateTime(local.Year, local.Month, local.Day,
local.Hour, local.Minute, 0, DateTimeKind.Local);Rely on 🔴 CRITICAL — Weekly schedule skips missed same-day slotsFile: if (candidateUtc <= now && i == 0) continue; // today's slot already passedIf today is a matching weekday and the scheduled time has passed, the slot is unconditionally skipped — even if the task hasn't run today. The Scenario: Weekly Mon-Fri 09:00, Impact: Weekly tasks miss ~1 day of execution per occurrence when the timer fires after the slot time. Fix: Mirror the Daily logic: check if 🔴 CRITICAL — UI edit overwrites run history (data loss race)File:
if (idx >= 0) _tasks[idx] = task; // replaces entire objectIf the background timer executed the task between Impact: Run history data loss whenever a task executes while the user has the edit form open. Fix: 🟡 MODERATE — Non-atomic file write risks total task lossFile: File.WriteAllText(TasksFilePath, json);If the app crashes or is killed during Fix: Write to a temp file, then atomic rename: var tempPath = TasksFilePath + ".tmp";
File.WriteAllText(tempPath, json);
File.Move(tempPath, TasksFilePath, overwrite: true);🟡 MODERATE — Disk I/O and serialization while holding lockFile:
Impact: UI thread freezes on Fix: Snapshot Notable Single-Model Findings (informational — did not meet 2-of-3 consensus)
📋 Summary
Recommended action: Four blockers:
Should also fix before merge: |
Review findings addressed (all from multi-model code review): 🔴 CRITICAL fixes: 1. Service eager start — ScheduledTaskService now resolved eagerly in MauiProgram.cs so the background timer starts on app launch, not just when the user visits the Scheduled Tasks page. 2. Cron off-by-one — GetNextCronTimeUtc now starts from the current minute (not +1). LastRunAt prevents re-firing within the same minute. 3. Weekly missed-day — Weekly schedule now mirrors Daily logic: only skips today's passed slot if LastRunAt shows the task already ran today. 4. Edit preserves run history — UpdateTask now merges only user-editable fields onto the canonical instance. LastRunAt and RecentRuns are never overwritten by stale clones from the edit form. 🟡 MODERATE fixes: 5. Atomic file write — SaveTasks uses write-to-temp + File.Move to prevent data loss on crash during write. 6. I/O outside lock — SaveTasks snapshots tasks under lock, then does serialization and file I/O outside the lock to prevent UI freezes. Additional hardening: - Start() checks _disposed guard to prevent zombie timers after Dispose - Per-task exception isolation in EvaluateTasksAsync foreach loop - Fixed 5 flaky timing tests in TurnEndFallbackTests using TCS pattern - Fixed minute-boundary race in CronSchedule_IsDue_ReturnsFalseWhenAlreadyRanThisMinute - Added tests: UpdateTask preserves run history, atomic write verification, Start-after-Dispose guard All 3,088 tests pass (0 failures). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
All 6 review findings addressed in commit ad10b28. See commit message for details. 3,088 tests pass, 0 failures. |
🤖 Multi-Model Code Review (Round 2) — PR #380feat: Scheduled tasks (cron jobs) for recurring prompt execution R1 Findings — Fix VerificationAll 6 findings from Round 1 are genuinely fixed with correct logic and good regression tests:
Fix details:
New Findings (Round 2)🟡 MODERATE — Shared temp file path creates save raceFile: var tempPath = TasksFilePath + ".tmp";
Fix: Use a unique temp file per call: var tempPath = TasksFilePath + $".{Guid.NewGuid():N}.tmp";🟢 MINOR —
|
| Finding | Model | Severity |
|---|---|---|
Impossible cron (0 9 31 2 *) holds _lock for ~50-500ms per eval cycle (527K iterations) |
Sonnet | 🔴 |
AddTask stores caller's reference directly — leaky abstraction |
Sonnet | 🟡 |
Start()/Dispose() race can create zombie timer (no memory barrier on _disposed) |
Sonnet | 🟡 |
run.Success = true means "prompt sent", not "AI responded" |
Opus | 🟡 |
| Cron DOM×DOW uses AND semantics; standard cron uses OR when both restricted | Codex | 🟢 |
| DST transitions may shift cron fire times (~1 misfire/year) | Opus | 🟢 |
✅ Positive Observations (all 3 models)
- Test coverage is excellent: 75+ tests covering model, CRUD, serialization, clone isolation, cron parsing, run history preservation, atomic write, and exact R1 regression scenarios
- Test isolation is proper:
SetTasksFilePathForTestinginTestSetup.csmodule initializer - Clone pattern is thorough: Deep copies
DaysOfWeek+RecentRuns;GetTasks()/GetTask()always return clones - CSS follows conventions: All
font-sizevalues usevar(--type-*)variables - Lazy property pattern:
TasksFilePathuses??=— nostatic readonlywith platform API calls - TurnEndFallbackTests improvements: Replaced fragile
Task.Delaytiming withTaskCompletionSourcesynchronization
📋 Summary
- CI:
⚠️ No checks configured for this branch - Prior comments: R1 review (6 findings) + author response (all addressed in commit
ad10b28) - R1 findings: All 6 verified fixed ✅
- New findings: 1 moderate (shared tmp path race), 1 minor (stale IsEnabled check)
- Test coverage: Strong — regression tests for all R1 fixes, clone isolation, cron edge cases
Recommended action: ✅ Approve
All 4 critical and 2 moderate R1 findings are genuinely fixed with good test coverage. The one new moderate finding (shared .tmp path) is a real race but low-probability in practice (concurrent saves within the same millisecond window) and has a trivial one-line fix that could be addressed in a follow-up. The PR is ship-ready.
Adds a cron-like scheduled tasks system for recurring prompt execution — daily stand-ups, periodic reviews, automated checks, etc. - Three schedule types: Interval (every N minutes), Daily (at time), Weekly (days + time) - Cron expression support: 5-field (min hour dom month dow) with wildcards, ranges, lists, and step values (e.g., '0 9 * * 1-5' for weekdays at 9am) - Full CRUD UI page at /scheduled-tasks with form validation - Task cards with toggle, edit, run-now, delete (with confirmation), and history - Expandable run history showing last 10 executions with timestamps and error details - Background timer evaluates due tasks every 30 seconds with overlap guard - Executes by sending prompt to existing session or creating a new one per run - Persists to ~/.polypilot/scheduled-tasks.json - Test isolation via SetTasksFilePathForTesting wired into TestSetup.cs - Fix Interval snap-forward: missed intervals return the last boundary, not 'now' - Fix Daily schedule: consistent local-time date comparison instead of mixed UTC/local - Fix SaveTasks race: RecordRunAndSave now holds lock through save - Fix CSS font-family enforcement: use var(--font-mono) not raw monospace - Fix .NET 10 Blazor type inference: use text input for time-of-day instead of type=time - Model: serialization, schedule descriptions, time parsing, due detection - Cron: valid/invalid expressions, ranges, steps, lists, JsonRoundTrip, next-run - Validation: IsValidTimeOfDay, ScheduleType.Cron enum, max interval - Schedule edge cases: daily never-run, daily already-ran-today, interval snap-forward - Service: persistence, CRUD, execution, error handling, corrupt file handling Fixes #367 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… canonical instance - ScheduledTask.Clone() deep-copies all fields including RecentRuns and DaysOfWeek - GetTasks() and GetTask() return clones so UI mutations cannot race with the timer - EvaluateTasksAsync collects task IDs (not direct references) before releasing lock - ExecuteTaskAsync(string taskId, ...) snapshots task data under lock, then executes async operations against the snapshot — no lock held across awaits - Convenience overload ExecuteTaskAsync(ScheduledTask, ...) delegates to ID-based version - RecordRunAndSave(string taskId, ...) looks up the canonical instance by ID under lock so stale UI clones can never corrupt the internal task's run history - 4 new tests: Clone independence, GetTasks/GetTask mutation isolation, stale-clone execution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Review findings addressed (all from multi-model code review): 🔴 CRITICAL fixes: 1. Service eager start — ScheduledTaskService now resolved eagerly in MauiProgram.cs so the background timer starts on app launch, not just when the user visits the Scheduled Tasks page. 2. Cron off-by-one — GetNextCronTimeUtc now starts from the current minute (not +1). LastRunAt prevents re-firing within the same minute. 3. Weekly missed-day — Weekly schedule now mirrors Daily logic: only skips today's passed slot if LastRunAt shows the task already ran today. 4. Edit preserves run history — UpdateTask now merges only user-editable fields onto the canonical instance. LastRunAt and RecentRuns are never overwritten by stale clones from the edit form. 🟡 MODERATE fixes: 5. Atomic file write — SaveTasks uses write-to-temp + File.Move to prevent data loss on crash during write. 6. I/O outside lock — SaveTasks snapshots tasks under lock, then does serialization and file I/O outside the lock to prevent UI freezes. Additional hardening: - Start() checks _disposed guard to prevent zombie timers after Dispose - Per-task exception isolation in EvaluateTasksAsync foreach loop - Fixed 5 flaky timing tests in TurnEndFallbackTests using TCS pattern - Fixed minute-boundary race in CronSchedule_IsDue_ReturnsFalseWhenAlreadyRanThisMinute - Added tests: UpdateTask preserves run history, atomic write verification, Start-after-Dispose guard All 3,088 tests pass (0 failures). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…afety The 1024-byte threshold was too tight — when xUnit runs DiagnosticsLogTests in parallel (12 Theory cases + 4 Fact tests share one log file), concurrent writes can exceed 1KB. Bumped to 10KB which still validates the test's intent (file is nowhere near the 10MB rotation threshold). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add BadgeHelper.cs (MacCatalyst) using UNUserNotificationCenter.SetBadgeCountAsync (Mac Catalyst 16+) with UIApplication fallback for 15.x - Track _pendingCompletionCount in CopilotService; increment in CompleteResponse for non-worker, non-active sessions only - Clear badge in SwitchSession, window.Activated, and OnResume so the count resets whenever the user returns to the app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
9179d1a to
65565a1
Compare
- RunNow: fire-and-forget ExecuteTaskAsync to avoid blocking the UI - SetEnabled: guard SaveTasks/OnTasksChanged with null check on task - IsValidTimeOfDay: reject negative TimeSpan values (add >= 0 check) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a cron-like scheduled tasks system for recurring prompt execution — daily stand-ups, periodic reviews, automated checks, etc.
Fixes #367
Features
0 9 * * 1-5for weekdays at 9am)/scheduled-taskswith form validation~/.polypilot/scheduled-tasks.jsonBug Fixes (from original Copilot bot PR)
nowvar(--font-mono)not rawmonospaceTests (71 total)
Files Changed
PolyPilot/Models/ScheduledTask.csPolyPilot/Services/ScheduledTaskService.csPolyPilot/Components/Pages/ScheduledTasks.razorPolyPilot/Components/Pages/ScheduledTasks.razor.cssPolyPilot/Components/Layout/SessionSidebar.razorPolyPilot/MauiProgram.csPolyPilot.Tests/ScheduledTaskTests.csPolyPilot.Tests/TestSetup.csPolyPilot.Tests/PolyPilot.Tests.csproj