Skip to content

Add scheduled recurring agent runs - #386

Open
0x92 wants to merge 6 commits into
dcouple:mainfrom
0x92:feature/scheduled-runs
Open

Add scheduled recurring agent runs#386
0x92 wants to merge 6 commits into
dcouple:mainfrom
0x92:feature/scheduled-runs

Conversation

@0x92

@0x92 0x92 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Let a project start agent sessions on a schedule: a fixed prompt, at a fixed time, in a fresh worktree.

image

Pane already knows how to create a session from a prompt — this makes that repeatable without a person present. Nightly bug sweeps, a weekly dependency check, a "review yesterday's diffs" pass every morning: the work that is worth doing regularly but never worth remembering.

A new Scheduled runs entry in the project's menu manages them.

What it does

  • Three schedule shapes rather than cron syntax: every N minutes, daily at HH:MM, weekly on a weekday at HH:MM. Each one can be stated in a sentence, which is what the dialog shows back to you (Every day at 03:00 — next run in 4h 12m). Cron is more expressive than anyone actually needs here and impossible to render as a sentence.
  • Each run creates a real session, exactly as the create dialog would: prompt, agent (claude, or none for a plain terminal) and an optional worktree/branch template.
  • Per-run state is visible: last run, its outcome, the error if it failed, and a link to the session it created.
  • Enable/disable without deleting, and Run now — which starts one immediately without moving the cadence, so testing a schedule at 15:00 does not shift the nightly run.
  • Missed runs are skipped, not caught up. Waking a laptop after a weekend must not start three days of nightly sweeps at once; a run more than 15 minutes late is recorded as skipped and the schedule moves on.
  • Deleting a project deletes its schedules (ON DELETE CASCADE).

How it works

  • scheduleCalculator.ts is pure and holds every decision about when: computeNextRun, isDue, isMissed. Time zones, daylight saving and "the next weekly run when today already passed" are the parts that break silently, so they are unit-tested directly rather than through the scheduler.
  • ScheduleManager ticks every 30s and asks the calculator; it talks to storage through a small ScheduleStore interface, so its behaviour is tested without a database.
  • Sessions are created through the existing task queue with createSessionAndWait, so lastSessionId is the real session id rather than a queue job id — the difference only shows up when you click the link.
  • Persistence is one table, scheduled_runs, plus an index on (enabled, next_run_at_ms) so the tick is a single indexed lookup.
  • Channels are daemon-owned (schedules:*): the schedule belongs to the machine that runs the agents.

Type of Change

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the CONTRIBUTING.md guidelines
  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have run pnpm typecheck and pnpm lint locally
  • I have tested the Electron app locally with pnpm electron-dev

Critical Areas Modified

  • State management/IPC events

Additional Notes

Schema note: schema.sql is split on the statement separator at startup, so a ; inside a comment breaks the database. The new block is written accordingly.

Tested in the running app, not only in unit tests: a schedule was created, fired on its own tick, produced a real session in a fresh worktree, recorded its outcome, and was then disabled and deleted. Missed-run handling was exercised by moving a schedule's due time into the past.

Automated QA

Status: Passed on d14e9878 with synthetic project Scheduled QA.

  • Playwright covered empty state, weekly form entry, save, pause/resume, Run now, persisted success state, and opening the resulting session.
  • Node 22 gates passed: root typecheck, full lint, and 65 focused main-process tests.
  • Native Electron dev launch succeeded with isolated data at /tmp/pane-pr386-qa. macOS accessibility automation was unavailable, so renderer interaction used the repository Playwright Electron API mock.
Empty state Weekly schedule form Run complete and paused
Empty scheduled runs dialog Filled weekly scheduled run form Paused scheduled run after successful Run now

Remaining human check: allow one real scheduled run to fire in a fresh worktree with the preferred installed agent.

0x92 added 2 commits August 23, 2026 12:20
start() called rescheduleAll(), which recomputed every enabled schedule's
next run from now. A stored time in the past was therefore replaced with
a future one before the first tick ever looked at it, so the tick had
nothing overdue to find: lastRunStatus never became 'skipped' and the
history showed no sign that the run had been due at all.

The same erasure took out runs that were meant to happen. A schedule two
minutes late is inside SCHEDULE_MISS_GRACE_MS and should start; instead
it was moved to the next occurrence and dropped. rescheduleAll also
rewrote the next run of schedules that were not overdue in any sense,
including one a user had just saved.

reconcileOnStart() now decides only the two things startup can decide: a
schedule with no next run at all gets one, and one that is late beyond
the grace period is written off as missed. Everything else keeps its
stored time and the tick decides, which is where that belongs. The
missed-run bookkeeping tick() already did moves into recordMissed() so
both paths write the same thing — one skipped entry per schedule, not one
per interval of downtime, because computeNextRun counts from now.

The existing tests drove tick() with a hand-built overdue row and never
called start(), which is exactly where the bug lived.

@parsakhaz parsakhaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request changes - four correctness defects violate the scheduled-run behavior stated in the PR.
Counts: Must Fix: 4 (security: 0) · Should Fix: 1 · pass 1/3

Must Fix

  • MF-1 - Startup does not immediately process a run that is inside the 15-minute grace window · main/src/services/scheduleManager.ts:55-72 · run the first tick immediately after reconciliation · violates the stated missed-run grace behavior
    • Evidence: start() only installs a 30-second interval. The startup test helper manually calls manager.tick() at main/src/services/scheduleManager.start.test.ts:63-68, so it does not exercise the actual launch sequence.
    • Failure scenario: A run is 14m50s late when Pane opens. It is still eligible at reconciliation, but the first interval fires 30 seconds later and records it as skipped.
  • MF-2 - In-flight execution is not coordinated with Run now, delete, disable, or edits · main/src/services/scheduleManager.ts:137-159 and main/src/services/scheduleManager.ts:209-240 · serialize by schedule id and merge execution results into the latest stored row · violates Run now starting one run and schedule mutation semantics
    • Evidence: runNow() bypasses the tick-level running guard, while execute() awaits session creation and later upserts the stale object it loaded before the await.
    • Failure scenario: A due tick and Run now can create two sessions. Deleting or disabling a schedule while its session is starting can later resurrect or re-enable it when execute() upserts its stale copy.
  • MF-3 - The daemon boundary accepts malformed schedule variants that can never run · main/src/ipc/schedule.ts:18-34 · validate kind, toolType, integer interval/weekday, project id, and parsed clock time · violates the three supported schedule shapes
    • Evidence: the regex accepts 99:99, unknown kind values fall through as weekly, and runtime IPC input is trusted as ScheduledRunInput. computeNextRun() then returns null for invalid clock values.
    • Failure scenario: A remote or renderer caller saves an enabled schedule successfully, but it has no next run and silently never starts.
  • MF-4 - The UI omits the promised link to the session created by the last successful run · frontend/src/components/schedule/ScheduledRunsDialog.tsx:167-172 · render lastSessionId as an action that closes the dialog and opens that session · violates the PR description under per-run state
    • Evidence: the row prints only time, status, and error even though lastSessionId is returned and persisted.
    • Failure scenario: A user sees a successful scheduled run but cannot navigate from the schedule to the session it created.

Should Fix

  • SF-1 - Rebase residue adds unrelated and inconsistent declarations · frontend/src/types/electron.d.ts:39 and main/src/preload.ts:135 · remove the unused GitDiffResult import and unrelated pr: prefix
    • Evidence: GitDiffResult has no references in electron.d.ts, and pr: is added only to the preload copy, not shared/types/daemon.ts.

Praise

  • The pure schedule calculator cleanly covers DST and cadence arithmetic.
  • SQLite uses bound parameters and a foreign-key cascade, with no injection issue found.
  • The focused suite passes: 4 files, 54 tests.

@parsakhaz
parsakhaz force-pushed the feature/scheduled-runs branch from 1184aa8 to d14e987 Compare August 23, 2026 19:53

@parsakhaz parsakhaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Approve - all four Must-Fix findings are resolved on d14e987.
Counts: Must Fix: 0 (security: 0) · Should Fix: 0 · pass 2/3

Must Fix

  • MF-1 fixed: startup performs an immediate tick, including the grace-boundary regression.
  • MF-2 fixed: execution locks by schedule id and merges results into the latest stored row.
  • MF-3 fixed: daemon input uses the shared boundary decoder plus domain validation.
  • MF-4 fixed: the dialog opens the last created session, covered by Playwright.

Praise

  • Root typecheck and full lint pass under Node 22.
  • Six focused test files pass with 65 tests.
  • The scheduled-run Playwright journey passes.

@parsakhaz

Copy link
Copy Markdown
Member

Review, simplify, refactor complete

Rebased onto current main and pushed final head d14e9878.

REVIEW

  • Posted a request-changes review with four correctness findings.
  • Fixed startup grace-window handling with an immediate tick.
  • Prevented concurrent duplicate runs and stale execution writes from resurrecting or re-enabling schedules.
  • Added shared boundary decoding and domain validation for schedule input.
  • Added the missing Open last session action.
  • Commit: e1dd708e fix(review): harden scheduled run execution
  • Re-review posted with approval after all findings were verified.

SIMPLIFY

  • Replaced conditional spreads and unchecked casts with explicit typed assignments and domain guards.
  • Moved IPC shape decoding to the shared boundary schema.
  • Removed dead schedule formatting code, dead exports, stale rebase imports, and redundant schedule clones.
  • Blocking lint improved from 28 PR-introduced errors before the pass to clean.
  • Commit: 5aa800f5 refactor(simplify): streamline scheduled runs

REFACTOR

  • Bounded the indexed repository query to schedules due at the current tick instead of loading every enabled future schedule every 30 seconds.
  • Added a repository contract test.
  • Commit: 87c0b031 refactor(schedules): query only due runs
  • Added durable renderer QA coverage for empty, create, save, pause/resume, Run now, and result-session navigation.
  • Commit: d14e9878 test(schedules): cover scheduled run dialog

Verification

  • PATH=/opt/homebrew/opt/node@22/bin:$PATH pnpm typecheck: passed
  • PATH=/opt/homebrew/opt/node@22/bin:$PATH pnpm lint: passed, including Oxlint, ESLint, boundary conformance, advisory scan, and Knip
  • Focused Vitest suite: 6 files, 65 tests passed
  • Playwright scheduled-run journey: 1 passed
  • Isolated Electron dev launch: passed with PANE_DIR=/tmp/pane-pr386-qa
  • Screenshot bytes were uploaded to the existing pr-assets release and verified by direct download and SHA-256. The PR description contains the gallery.

Follow-ups

  • Split the 366-line scheduled-runs dialog into orchestration, row, and form units after adding component-level interaction tests.
  • Decide and document whether multiple schedules due together should be enqueued concurrently before changing current sequential execution.
  • Add a native SQLite integration suite for row mapping, upsert behavior, and project cascade deletion.

Left for parsa

  • Let one real scheduled run fire with the preferred installed agent and confirm it creates a fresh worktree and opens from the saved session link.
  • Merge when satisfied. No merge was performed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants