Repository navigation
feat(core): scheduled tasks storage and time triggers (experimental) - #59401
kitlangton wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
I didn't find anything that should block this. The 12 tests in test/schedule.test.ts pass, and core, schema and server type-check (with tsc directly, because this Bun version has no bun check). I couldn't drive it live: nothing outside the service can create schedules until PR 2. I also checked the dedupe path: re-sending the same msg_${scheduleID}_${run} id after a crash does nothing, even if the first prompt was already moved into the transcript.
Public API: two new optional config keys, top-level timezone (string) and experimental.schedules (boolean, off by default). They appear in schema.json, openapi.json and the generated client types. Also added: the @opencode/schema/schedule module and a schedule table migration. No HTTP routes, SDK methods, events or CLI flags change, so this is backwards compatible.
One thing worth fixing, inline: due schedules keep booting their session's location on every tick after experimental.schedules is turned off.
There was a problem hiding this comment.
The new commit fixes the problems its title names: the last slot after ends_at, the repeated hour when clocks go back, crons that can never fire, [Zone] suffixes on at times, pausing after the tick read the row, lost wakes (on start and on each skip), and schedules that fall due mid-firing. I found no bugs in them. 19 tests in test/schedule.test.ts pass, and tsc -b passes in core and server.
The earlier point about settings() booting the location on every tick while the flag is off isn't addressed, so that thread stays open. New: one test depends on a fixed number of scheduler turns (inline). Public API: no change since the last review.
There was a problem hiding this comment.
The new commit makes cron timing around clock changes match standard cron: wildcard and step crons keep firing through the repeated hour, fixed-time crons fire once, and a slot in the skipped hour moves forward. I found no problems with it. 21 tests in test/schedule.test.ts pass, and tsc -b passes in core.
Neither earlier thread is addressed: the per-tick location boot while the flag is off, and the 50-yieldNow wait in the re-tick test. Public API: no change since the last review.
There was a problem hiding this comment.
Both earlier points are addressed. Due schedules in a location with the flag off are now paused instead of booting the location every tick. The re-tick test now waits until both prompts are pending instead of counting scheduler turns. 22 tests in test/schedule.test.ts pass, and tsc -b passes in core. Public API: no change since the last review.
One side effect of the new pause is noted inline.
There was a problem hiding this comment.
The flag-off pause point is addressed. Schedules now stay active with their due time, are rechecked hourly through retry_at, and catch up once the flag is back on. The trade-off is up to an hour's delay after re-enabling, which seems fine for an experimental flag. 22 tests in test/schedule.test.ts pass; tsc passes in core and schema.
Public API: no config or HTTP change since the last review. Schedule.Outcome in @opencode/schema/schedule gains "disabled" (backwards compatible). The migration was renamed to 20261026223005_schedule and adds a retry_at column. Only databases that ran this branch's earlier build would notice the rename.
…ed zones, back off failed prompts
There was a problem hiding this comment.
Nothing blocking in the new commit. The repeated-hour handling now never fires a slot twice; this differs from standard cron, but the description explains it. The step-forward loop always makes progress and stops. A failed prompt backs off for 5 minutes, and the retry reuses the same message id, so a prompt admitted before the failure isn't sent twice. Zero durations and ends.times below 1 are now rejected. Without the re-read before firing, a pause or remove that lands mid-tick can still let one firing through; the window is short, so that seems fine. 23 tests in test/schedule.test.ts pass; tsc passes in core and schema.
Public API: config keys unchanged. Schedule.Outcome in @opencode/schema/schedule swaps "expired" for "failed"; neither has shipped, so nothing outside this branch is affected.
…pace-separated at offsets
A schedule can now wake a session with a prompt: once, at a set time, every N minutes, or on a cron, behind an experimental flag.
This PR adds only the storage and the timer. Agents and apps can't create schedules yet; that comes in the next PR.
💡 Why
Long-running work like "check error rates every 10 minutes for the next 8 hours" needs the session to wake on its own, not wait for someone to type.
🆕 Now you can
when(in, at, every, cron, with an optional time zone),ends(after, at, times) and a prompt to send.🚦 How it ships
Off by default. Turn on
experimental.schedulesin config. With it off, creating a schedule fails and the timer never fires. An optional top-leveltimezonesetting picks the zone when a schedule doesn't name one; otherwise the server's own zone is used.🕐 Clocks falling back: a cron slot never fires twice in the repeated hour.
Cron.nextpicks the first pass of a repeated time; if that's already behind us, we step forward an hour until a slot is ahead. So*/10runs through the first 1:00–1:59 and resumes at 2:00, rather than firing through both.↩️ Undo: revert isn't enough, because the migration has already created the
scheduletable in every database that ran this build. The table is harmless and empty unless the flag was turned on, but removing it needs a follow-up migration.👀 Read:
packages/core/src/schedule.ts, thefireandstepfunctions. The schedule row is updated only after the prompt is admitted, and the prompt id comes from the run number, so a crash in between re-sends the same id instead of a duplicate. The rest is routine.Planned feature · no user reports · not measured
Checks run
bun run script/test.ts test/schedule.test.ts(core): 24 pass, covering a pause during firing,attimes with a space before the offset, one-shotinandat,everywithends.timesandends.at, cron across the Nov 1 DST change and inside the repeated hour, one late catch-up, the 5-minute backoff after a failed prompt, rejecting a zero duration andends.timesbelow 1, overlap skip, crash between admit and update, session delete cascade, flag off, pause/resume, and the loop waking on its timerbun typecheckin core, schema and server: passAdded by the opencode bot (AI-generated).
Related: #41906, #53960
Public API: adds optional config keys
timezoneandexperimental.schedules(backwards compatible)