feat(core): cron tools for scheduled prompts by paolodelia99 · Pull Request #44191 · anomalyco/opencode · GitHub
Skip to content

feat(core): cron tools for scheduled prompts - #44191

Closed
paolodelia99 wants to merge 37 commits into
anomalyco:devfrom
paolodelia99:feat/cron-tools
Closed

paolodelia99 wants to merge 37 commits into
anomalyco:devfrom
paolodelia99:feat/cron-tools

Conversation

@paolodelia99

Copy link
Copy Markdown

Issue for this PR

Partially Closes #41907, #18001

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

This PR adds support for scheduled prompts, a feature missing in opencode.
Inspired by Claude code’s /loop, here’s my attempt to bring scheduled prompts to opencode (under the name of cron), a feature that in some cases might be useful to developers. This is a first PR that exposes the scheduling of CronJobs through the brand-new cron_add, cron_list, cron_delete tools; the usage of those tools is exposed (like in Claude code) through a built-in skills (which is scheduling.md). Those tools then interact with the CronService, which, as the name suggests, handles various CronJobs for various sessions and stores them in a PriorityQueue.

The CronService, in turn, uses the CronDeliveryPort to dispatch those CronJobs to the current prompt session.

Please note that the CronService has been thought to be used in the TUI and in the opencode app; I didn’t want to expose it to the SDK, since I don’t see any usage of that through the SDK.

How did you verify your code works?

You can directly try to use the cron_add tool from the TUI like using:

cron_add 1m Say "Hello, world!"

or with more natural language:

Add a new cron job that triggers every 2m with prompt Say "Hello, World!"

Other examples:

Add a scheduled prompt that every 2 min runs the typechecking on this repo
Add a scheduled prompt that runs every 5 min that checks if all the tests in the @packages/core/ module are passing

Screenshots / recordings

No, UI changes.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

…s them

Effect.catchCause ran before Effect.catch, so typed CronDeliveryError
failures were caught by catchCause's non-interrupt branch and logged as
defects. Swap the pipe order so catch handles typed errors first and
catchCause only sees true defects.
Add regression tests for the sooner-job precedence fix (8ef9510),
busy-port reschedule, exists=false rejection, non-defect delivery
failure logging, parseDuration boundary (60s, zero-prefixed, all-zero),
cron_add/list/delete round-trips, and legacy port exists/isBusy-true/
deliver-error-mapping branches.
…e to now+interval

- add: set nextRunAt = now so the loop picks up the job immediately
- busy branch: poll with 10s retry when runCount === 0 until session
  is free, then fire the first run and anchor the cadence
- busy branch: reschedule to now + interval (not nextRunAt + interval)
  on subsequent busy ticks so the cadence re-anchors at the busy moment
- post-delivery: set nextRunAt = current + interval to prevent drift
The core cron node (V2 port) and core tool/cron.ts were dead code.
The opencode app defines its own cron tools via Tool.define and wires
CronService with the legacy port. The server never exposes cron
endpoints so these core definitions were never exercised.
Replace the isBusy-check-and-reschedule branch with service-level
delivery coalescing. At each tick the loop advances nextRunAt by
intervalMs (reverting the earlier current+interval change), re-inserts
the job, and forks the delivery; the legacy runner queues a delivery
behind a busy session via ensureRunning. A Ref<Map<jobId, Fiber>> tracks
pending deliveries so at most one is queued per job, and remove()
interrupts a pending delivery fiber. Removes BUSY_RETRY_MS.
A magnitude such as a 400-digit hour count overflows the millisecond
computation to Infinity, which slips past the minimum-interval floor and
reaches the scheduler as a sleep(Infinity). Reject non-finite results
with a CronError.
@Enough1122

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot closed this Sep 22, 2026
hunterchristian added a commit to chipp-ai/opencode that referenced this pull request Sep 23, 2026
A monitor tool (plus monitor_list/monitor_stop) lets the model watch
a long-running background command's output for a pattern, or until it
exits, and get notified back into the session as new messages -- a
standing watch, not just a one-shot check. Analogous to Claude Code's
own Monitor tool. Gated behind OPENCODE_EXPERIMENTAL_MONITOR, matching
the existing experimentalBackgroundSubagents convention.

Ported from upstream anomalyco/opencode PR anomalyco#33806 "feat: add Monitor
tool" (1,398 lines, closed by the automated PR-cleanup bot, never
reviewed) -- built directly on this fork's existing BackgroundJob
primitive rather than modifying it. Reused nearly word-for-word: the
re-arm/replace parameter shape, session-scoped list/stop logic, the
untrusted-output fence with an unpredictable id so watched output
can't forge its own closing tag, control-character stripping, the
never-grant-"always"-for-a-glob-command permission rule, the
detached-process-plus-parent-death-watchdog design, the flood guard,
and most of the tool description and test scenarios. Rewritten rather
than ported: avoided anomalyco#33806's own change to core BackgroundJob (a
WakeScope to dodge a self-re-arm deadlock) by forking notifications
into the tool's own scope instead, the same pattern task.ts already
uses; its hand-written reader/debounce became an Effect Stream
pipeline; its base64 command-wrapper isn't needed since this repo's
bash tool already runs commands through plain ChildProcess.make;
permission goes through the existing bash permission rather than a
new one, so existing bash rules govern monitors too.

Also considered anomalyco#44191 (cron scheduler, closed by the same bot the
same week, praised by a human reviewer first) and the
anomalyco#47166/anomalyco#47193/anomalyco#47284 durable-heartbeat resubmits (each closed within
hours by a different, 2-hour PR-template-compliance bot, never
reviewed) -- neither ported; see known gaps.

Found and fixed a real, generalizable bug along the way: a command
that writes output continuously with no pause (e.g. `yes`) hung
teardown and starved timers, because the stdout read pipeline never
yielded control back to the scheduler while data kept arriving.
Fixed with a single Stream.tap(() => Effect.yieldNow), documented
inline -- the same issue could affect other handle.stdout readers
facing a non-pausing producer.

Known gaps (deliberately scoped): not durable, in-memory only; no
scheduler (only a shell poll-loop inside the watched command); opencode
run's non-interactive mode exits once idle, killing active monitors;
no UI (model-only via monitor_list); stdout only, 4000-char line cap,
5000-line flood guard; parent-death watchdog is POSIX-only; no
external/webhook delivery.

New packages/opencode/src/tool/monitor.ts (~360 lines) + monitor.txt.
New packages/opencode/test/tool/monitor.test.ts (13 tests: self-cancel
re-arm without deadlock, concurrency, list/stop, byte-flood guard,
multi-line commands) -- passes 3 runs in a row. Verified against this
fork's current dev (not just the agent's own stale worktree base):
bun typecheck clean; full packages/opencode suite 3600 pass, only the
4 pre-existing, confirmed-unrelated cf-ai-gateway failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: /loop — dynamically repeat a command N times

2 participants