Skip to content

agents: run cron Scheduler inside the long-running daemon (HIR-119)#10

Merged
pypesdev merged 1 commit intomainfrom
scheduler-daemon-loop
May 4, 2026
Merged

agents: run cron Scheduler inside the long-running daemon (HIR-119)#10
pypesdev merged 1 commit intomainfrom
scheduler-daemon-loop

Conversation

@jaredzwick
Copy link
Copy Markdown
Collaborator

Summary

  • Spawns a tokio task on pypes start that loads every Action::Cron entry across all agents on boot, sleeps until the next due fire, dispatches the wrapped webhook through the existing executor, advances the scheduler, and logs each fire to ~/.agents/tmp/daemon.err.
  • POST /agents now triggers an in-process scheduler reload via tokio::sync::Notify so newly stored cron actions go live without restarting the daemon.
  • Closes the loop opened by HIR-118 — the cron executor is now actually user-visible: agents can finally "check inbox every 10 minutes" or "run nightly summary".

Closes HIR-119. Follow-up to HIR-118 (#9).

What changed

  • src/scheduler_loop.rs (new): SchedulerHandle + tokio loop with select! over Notify::notified() (reload signal) and Scheduler::next_due(). Empty schedule parks on pending::<()>().await so reloads still wake it.
  • src/server/server.rs: introduces AppState { db, scheduler }; serve() spawns the scheduler loop on the same tokio runtime that hosts axum, before binding the listener.
  • src/server/handler.rs: agents_create calls state.scheduler.reload() after the db write.
  • src/lib.rs: re-exports scheduler_loop.
  • README.md: drops the "single-process / fires only while running" caveat from the ## Action Executors → Cron section and documents that firing happens automatically once pypes start is up.
  • CHANGELOG.md: Unreleased entry.

Tests

cargo test — 21 passed (was 18). Two new wiremock-backed integration tests in src/scheduler_loop.rs:

  • loop_fires_stored_cron_action_against_mock_receiver — stores an agent with a * * * * * * cron action pointing at a mock, spawns the loop, asserts the mock is hit within 2.5s.
  • reload_picks_up_newly_added_agents — starts with an empty db, confirms no fires, then writes the cron agent + calls handle.reload(), asserts the mock is hit within 2.5s.
  • collect_cron_actions_returns_only_cron_entries_across_all_agents — unit-level coverage for the db walker.

Manual verification

# 1. Build + start a daemonized server
cargo build --release
./target/release/pypes start -p 7989

# 2. POST a cron agent firing every second at a local mock receiver
curl -s -X POST http://127.0.0.1:7989/agents \
  -H 'Content-Type: application/json' \
  -d '{"name":"ticker","inputs":[],"actions":["{\"type\":\"cron\",\"expression\":\"* * * * * *\",\"action\":{\"type\":\"webhook\",\"url\":\"http://127.0.0.1:18181/hook\",\"payload\":{\"event\":\"manual.tick\"}}}"]}'

# 3. Observe ~/.agents/tmp/daemon.err

Observed log output (excerpt):

[scheduler] started with 0 cron action(s)
[scheduler] reloaded with 1 cron action(s)
[scheduler] entry 0 `* * * * * *` fired webhook http://127.0.0.1:18181/hook → status=200 (11 bytes)
[scheduler] entry 0 `* * * * * *` next fire 2026-05-03 09:23:35 UTC
[scheduler] entry 0 `* * * * * *` fired webhook http://127.0.0.1:18181/hook → status=200 (11 bytes)
[scheduler] entry 0 `* * * * * *` next fire 2026-05-03 09:23:36 UTC
[scheduler] entry 0 `* * * * * *` fired webhook http://127.0.0.1:18181/hook → status=200 (11 bytes)
[scheduler] entry 0 `* * * * * *` next fire 2026-05-03 09:23:37 UTC

Mock receiver got three POSTs spaced one second apart, exactly matching the cron expression.

Out of scope (per ticket)

  • Distributed / clustered scheduling.
  • Persistent missed-fire catchup across daemon restarts (loop starts fresh from Utc::now() on boot).
  • LLM executor.
  • Per-tenant isolation.
  • Scheduler reload on the CLI mutation paths (pypes add agent, pypes rm agent) — those bypass the daemon process; reload remains scoped to the HTTP API surface as the ticket requested.

Test plan

  • cargo test passes locally
  • Manual: pypes start daemonized + POST /agents fires the cron action against a mock receiver and logs each fire to ~/.agents/tmp/daemon.err
  • CI pr_check workflow green

Spawns a tokio task on `pypes start` that loads every Action::Cron entry
across all agents on boot, sleeps until the next due fire, dispatches the
wrapped webhook through the existing executor, advances the scheduler,
and logs every fire to the daemon's stderr. POST /agents triggers an
in-process reload via tokio::sync::Notify so newly stored cron actions
go live without restarting the daemon.

- src/scheduler_loop.rs: SchedulerHandle + tokio loop with select! over
  reload signal and Scheduler::next_due
- src/server/server.rs: AppState wraps db + SchedulerHandle; serve() spawns
  the loop before binding the listener
- src/server/handler.rs: agents_create signals reload after writes
- README: drop the single-process caveat; document automatic firing
- CHANGELOG: Unreleased entry
- Integration tests use wiremock to assert the loop fires stored cron
  actions and reloads pick up newly added agents

Closes HIR-119.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@jaredzwick
Copy link
Copy Markdown
Collaborator Author

CTO review — see HIR-119 thread for full notes. tl;dr: approved, CI green, ready to merge once Jared manually verifies on his box. Two non-blocking follow-up suggestions captured in the issue thread (CLI mutation reload + scheduler error logging).

@pypesdev pypesdev merged commit 182c9c8 into main May 4, 2026
1 check passed
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