0080 - Cron
- Feature Name: Daemon-Level Cron Scheduler
- Start Date: 2026-03-20
- Discussion: #80
- Crates: daemon
Summary
A daemon-level cron system that triggers skills into sessions on a schedule, replacing the previous per-agent heartbeat mechanism.
Motivation
Agents need periodic behavior — checking feeds, running maintenance, sending reminders. The original approach was a per-agent heartbeat config, but this was dead code and wrong-shaped: heartbeats are uniform intervals, while scheduled tasks need cron-style flexibility (every Monday at 9am, every 2 hours, etc.).
The session already carries the agent and sender. A cron entry only needs to know which skill to fire and which session to fire it into.
Design
A cron entry triggers a skill into a session on a schedule.
Data model
[[cron]]
id = 1
schedule = "0 */2 * * *"
skill = "check-feeds"
session = 12345
quiet_start = "23:00"
quiet_end = "07:00"
once = false
id— auto-incremented on create.schedule— standard cron expression, validated on create and load.skill— fired as/{skill}slash command into the session.session— target session ID. The session determines the agent.quiet_start/quiet_end— optional HH:MM window in the daemon’s local time. If fire time falls inside, skip silently. No queuing, no catch-up. Both must be set; if only one is provided, quiet hours are ignored.once— fire once then delete from memory and disk.
Persistence
Memory is authoritative at runtime. Disk (crons.toml) is recovery for
restarts.
- Startup: load from disk, start timers. Invalid schedules are skipped with a warning.
- Create/Delete: mutate memory, start/stop timer, atomic write to disk (tmp + rename).
- Runtime reload: crons stay in memory — they survive runtime swaps.
- Daemon restart: reload from disk.
Firing
Fire-and-forget via the daemon event channel. The cron sends a ClientMessage
with content /{skill} and sender "cron". The reply channel is dropped —
output goes to session history only.
Protocol
Three protocol operations on the Server trait:
CreateCron { schedule, skill, session, quiet_start?, quiet_end? }→CronInfoDeleteCron { id }→ success/not foundListCrons→CronList
Crons are process-lifetime, not session-lifetime. They survive runtime reloads, fire via the daemon event channel, and the runtime has no notion of time-based scheduling. This is a daemon concern.
Alternatives
Per-agent heartbeat config. The original approach. Rejected because it coupled scheduling to agent definition, couldn’t express cron-style schedules, and was dead code.
Client-side polling. A client can send messages on its own timer. This works but requires the client to be running. Daemon crons fire regardless of client state.
Unresolved Questions
- Should crons support arguments beyond the skill name?
- Should there be a max cron count to prevent resource exhaustion?