Skip to content

[OMEGA-313] feat: scheduled agent tasks (omegaclaw-cron) - #276

Open
amiroussama wants to merge 2 commits into
singnet:mainfrom
amiroussama:contrib/scheduler
Open

[OMEGA-313] feat: scheduled agent tasks (omegaclaw-cron)#276
amiroussama wants to merge 2 commits into
singnet:mainfrom
amiroussama:contrib/scheduler

Conversation

@amiroussama

Copy link
Copy Markdown

Description

Fixes #275. Adds a scheduler for time-based agent tasks.

  • src/scheduler.py — cron / interval / one-shot jobs and webhook subscriptions, persisted in a SQLite jobs DB, with HMAC-verified webhook delivery. Job runs are recorded via the session store; stored text is scrubbed via the shared redactor. is_safe_skill_name is inlined (validates job/subscription ids that become filesystem path segments) so the scheduler carries no dependency on the skills subsystem.
  • scripts/omegaclaw-cron — CLI to add / list / remove / run jobs.

Depends on #266 (redaction) and #274 (session_store) — both files are included in this branch and drop out on rebase as those PRs merge.

How Has This Been Tested?

Autotests/test_scheduler.py (pure-Python, stdlib sqlite3/hmac, host-runnable), registered in run_mandatory — cron/interval/one-shot scheduling + next-run computation, job add/list/remove/run, webhook HMAC verification (valid/invalid signatures), the transient-webhook-id-never-deletes-a-durable-job regression, and id path-safety. Run: cd Autotests && python3 test_scheduler.py → 16/16 pass.

Checklist

  • The code generated by LLM is reviewed by the PR creator
  • Self-review completed
  • Test scenarios above are passed with the version of the code from PR

@vsbogd vsbogd added the plugin label Jul 23, 2026
… + webhooks

Adds a scheduler for time-based agent tasks: cron / interval / one-shot jobs and
webhook subscriptions, persisted in a SQLite jobs DB, with HMAC-verified webhook
delivery and the `omegaclaw-cron` CLI to add/list/remove/run jobs. Job runs are
recorded via the session store for auditability.

`is_safe_skill_name` is inlined (validates job/subscription ids used as filesystem
path segments) so the scheduler carries no dependency on the skills subsystem.

Depends on singnet#274 (session_store) — included in this branch and drops out on rebase
once it merges. Pure-Python, stdlib sqlite3/hmac.
Autotests/test_scheduler.py (host-runnable) + run_mandatory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alyona-snet alyona-snet changed the title feat: scheduled agent tasks (omegaclaw-cron) [OMEGA-313] feat: scheduled agent tasks (omegaclaw-cron) Aug 5, 2026
@alyona-snet alyona-snet added the in-jira The issue has been accepted for fixing label Aug 5, 2026

@paul-v-snet paul-v-snet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It looks like this does not actually work.
Similarly to PR #274, the implemented module looks isolated within itself, and I don’t see a single entry point for either the agent or the user.

The CLI tool allows creating a job, but this does not initiate its automatic execution (via tick/run_due or anything else). So it looks like a job manager has been implemented, but not an operator for those jobs. What I mean is that I don’t see any mechanisms that would actually operate them - timers, counters, or anything else. This would be fine if there were some component that periodically called tick or run_due on its own, but nothing does so: the agent - there are no connections to it in the form of the necessary skills or shell instructions, the main loop - no changes were made to it, the user - as far as I can see, cmd_tick is not connected to any loop anywhere in the project, so the only way to trigger a check is to manually execute this command inside the agent container, which does not look like the intended usage scenario.

Even if someone were to call cmd_tick at the required interval, the result would not reach the agent proactively. delivery_fn defaults to None, in which case delivery is simply skipped silently. For alert_fn, there is a stub that calls print() to the stdout of the process that invoked tick, but this does not solve the problem either.
Similarly, regarding runners, only _default_runner is implemented, which just echoes the prompt and does nothing else.

So, in the end, we have a mechanism for creating and scheduling jobs, but no way to actually operate them, automatically trigger their execution, or automatically receive their results (via a callback or alert). This makes it unclear what the intended role of this component is and what it can actually do in its current implementation.

Additionally, during job execution (_execute_one), a working directory is created (which is not used afterwards and is not managed from anywhere), but there is no check that this directory is accessible for reading/writing according to the profile/policy.
Moreover, os.makedirs() is called outside the try/except block that wraps the rest of the job execution. An unhandled exception at this step would bring down the entire run_due(), including other jobs that were supposed to run during the same tick.
And since the CLI create command does not allow specifying --workdir at all, a new temporary directory is created via tempfile.mkdtemp on every execution and is never removed - meaning this also results in an unbounded growth of the number of directories on disk over time.

Could you please provide clarification on all of these points?

Thank you.

…mp-dir leak

Addresses PR singnet#276 review (paul-v-snet): the module scheduled jobs but nothing
operated them, and per-run setup could abort a whole tick.

- run_forever() + `omegaclaw-cron daemon`: the operator that loops run_due() so
  due jobs actually fire on their own (bounded by --max-ticks; injectable
  sleep_fn for deterministic tests).
- Durable default delivery/alert sinks (memory/cron_outbox.jsonl) so a run's
  output/failure is captured instead of silently dropped when no hook is wired.
  Real channel push remains the documented extension seam.
- _execute_one: workdir resolution + os.makedirs + runner now run inside one
  try/finally, so a bad job fails ONLY itself and can't abort the run_due tick.
- Ephemeral temp workdirs are removed after each run (no unbounded /tmp growth);
  an explicit --workdir is validated against the io-policy read_write roots.
- CLI: `create --workdir/--delivery`, new `daemon` subcommand, `tick` wires the
  default sinks.
- Tests: +5 (failure isolation, temp-dir cleanup, workdir policy, daemon fires a
  due job, outbox delivery). 21/21 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@amiroussama

Copy link
Copy Markdown
Author

@paul-v-snet thanks for the careful review — you're right on every point, and the core one is fair: this shipped the job store + scheduling primitives but nothing that actually operates them. Pushed a fix (b06ba43) addressing all of it:

The missing operator. Added run_forever() and an omegaclaw-cron daemon subcommand — a heartbeat that loops run_due() on an interval so due jobs fire on their own. Previously the only trigger was a human running tick by hand, exactly as you noted. --max-ticks bounds it and sleep_fn is injectable so tests drive it deterministically.

Results reaching somewhere. delivery_fn/alert_fn now default to a durable sink (memory/cron_outbox.jsonl) instead of being silently skipped / printed to a dead stdout. tick and the daemon wire these by default, and create gained --delivery. A run's output/failure is now captured rather than dropped. Driving a real agent run from the runner is intentionally left as the documented extension seam (_default_runner → inject a real runner) — I kept that out of scope for this PR rather than half-implement it; happy to open a follow-up issue for it if you'd prefer it tracked.

The run_due crash. os.makedirs (and workdir resolution + the runner) now live inside one try/finally, so a bad job fails only itself and can no longer abort the whole tick and take down the other due jobs.

Temp-dir leak. An ephemeral temp workdir is now removed after each run (no more unbounded /tmp growth on every interval fire). Jobs can pass an explicit --workdir, which is validated against the io-policy read_write roots (fails closed on a path outside them; fails open only when the policy is undeterminable, e.g. a bare host).

Tests: +5 covering failure isolation, temp-dir cleanup, workdir policy (in/out/undeterminable), the daemon firing a due job, and outbox delivery — 21/21 pass (cd Autotests && python3 test_scheduler.py).

Let me know if the runner-seam scoping works for you or if you'd rather see a minimal real runner in this PR too.

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

Labels

in-jira The issue has been accepted for fixing plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[OMEGA-313] Scheduled agent tasks (cron/interval/one-shot + webhooks)

4 participants