Skip to content

feat(cron): goal-driven auto-disable usercron jobs (implements #816)#818

Merged
thepagent merged 5 commits into
mainfrom
feat/usercron-disable-on-success
May 21, 2026
Merged

feat(cron): goal-driven auto-disable usercron jobs (implements #816)#818
thepagent merged 5 commits into
mainfrom
feat/usercron-disable-on-success

Conversation

@chaodu-agent
Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent commented May 13, 2026

Implements the design from #816 (ADR: goal-driven cronjob).

Summary

Adds goal-driven cronjobs — scheduled tasks that automatically disable themselves once a goal is achieved.

Motivation

Today, cronjobs fire indefinitely on a schedule. But many real-world use cases are goal-oriented: "keep running tests and fixing code until they pass." Once the goal is met, the job should stop. Without this, users must manually disable jobs or the agent keeps working on a solved problem.

How It Works

A usercron job can define a completion check — a command that runs before each scheduled prompt. If the command exits 0 AND its output contains a configurable marker, the goal is considered complete:

[[jobs]]
id = "fix-unit-tests"
enabled = true
schedule = "*/10 * * * *"
channel = "1490282656913559673"
message = "Unit tests are still failing. Continue fixing them."

disable_on_success = "npm test && echo OPENAB_GOAL_SUCCESS"
disable_on_success_match = "OPENAB_GOAL_SUCCESS"
disable_on_success_timeout_secs = 120
disable_on_success_working_dir = "/workspace/my-project"

Execution Flow

Schedule fires
    │
    ▼
Run disable_on_success command
    │
    ├─ exit 0 + marker found → ✅ Post "Goal achieved", write enabled=false, STOP
    │
    └─ otherwise → Send scheduled message, agent continues working

Why Both Exit Code AND Marker?

Plain exit 0 is too easy to satisfy accidentally (e.g., npm test exits 0 if no tests exist). The marker (OPENAB_GOAL_SUCCESS) is an explicit signal that the specific goal was met, not just that a command ran without error.

Implementation

  • Completion check: runs disable_on_success command with configurable timeout and working directory
  • Scheduler writeback: persists enabled = false and thread_id back to $HOME/.openab/cronjob.toml by stable id
  • Scope: usercron-only ([[jobs]] in external file), not baseline [[cron.jobs]] — keeps writeback limited to user-managed files

New Usercron Fields

Key Type Default Description
id string required Stable job ID for scheduler writeback
disable_on_success string Command to run as completion check
disable_on_success_match string required Marker that must appear in output
disable_on_success_timeout_secs integer 60 Timeout for the check command
disable_on_success_working_dir string Working directory for the check

Use Cases

  • Fix CI: "Run tests every 10 min, keep fixing until green"
  • Deploy: "Check deployment health every 5 min until all pods ready"
  • Migration: "Run migration script until all records processed"
  • Code review: "Check PR status every 15 min until approved and merged"

Re-enabling a Disabled Job

When a goal is achieved, OAB writes enabled = false to $HOME/.openab/cronjob.toml. To restart the job:

# In cronjob.toml — flip back to true
enabled = true

This is currently a manual edit (or the AI agent can do it if asked). A future enhancement could add a slash command (e.g. /cron enable fix-unit-tests) or allow the agent to re-enable jobs via tool call.

Tests

  • git diff --check
  • Focused cron unit tests added

Discord thread: https://discord.com/channels/1491295327620169908/1504239931940409587

Comparison with Other Agents

OAB (this PR) Hermes OpenClaw
Goal check External command + marker Judge model evaluates response N/A
Trigger Cron schedule Every turn (continuous) Cron schedule
On success Disable job, persist to file Stop loop Delete one-shot job
Customizable check ✅ Any command ❌ Built-in judge
Resumable Edit enabled = true Pause/resume Re-create job

Pros and Cons

OAB (external command) Hermes (judge model) OpenClaw (one-shot delete)
Pros Verifiable — shell command is deterministic, auditable Zero config — just state the goal in natural language Simple — no extra config needed
Works for anything testable (CI, health checks, API calls) Continuous — doesn't wait for next cron tick Low overhead
No LLM cost for the check itself Adapts to ambiguous goals
Decoupled — check is independent of the agent's work
Cons Requires a testable condition (not all goals are scriptable) Burns tokens on judge calls every turn No retry — runs once and done
Latency — waits for next cron tick to check Judge can hallucinate success No custom completion criteria
User must write the check command Harder to audit — "why did it stop?" Can't handle "keep trying until X"

Best for: Engineering goals with clear pass/fail signals (tests pass, deploy healthy, migration complete).

@chaodu-agent chaodu-agent force-pushed the feat/usercron-disable-on-success branch from 99e406d to f8d3d16 Compare May 13, 2026 23:11
@shaun-agent
Copy link
Copy Markdown
Contributor

OpenAB PR Screening

This is auto-generated by the OpenAB project-screening flow for context collection and reviewer handoff.
Click 👍 if you find this useful. Human review will be done within 24 hours. We appreciate your support and contribution 🙏

Screening report ## Intent

PR #818 tries to let usercron jobs automatically turn themselves off once a configured goal has been reached.

The operator-visible problem: today, a goal-oriented scheduled job can keep running even after it has succeeded, causing repeated agent runs, noise, wasted compute, and possible repeated Discord/thread activity. This PR adds a completion check so a job can prove success and then persist enabled = false back to $HOME/.openab/cronjob.toml.

Feat

Feature.

Behaviorally, usercron jobs may define a disable_on_success command plus a required output marker via disable_on_success_match. The job is considered complete only when the command exits 0 and stdout/stderr contains the marker. On success, the scheduler writes back to the user cron TOML by stable id and disables the job.

It also persists thread_id after thread creation and documents the new fields.

Who It Serves

Primary beneficiary: agent runtime operators and deployers running goal-driven scheduled jobs.

Secondary beneficiaries: maintainers and reviewers, because completed recurring work becomes explicit state instead of ambient scheduler behavior.

Rewritten Prompt

Implement goal-completion auto-disable for usercron jobs only.

Add optional usercron fields:

disable_on_success = "command"
disable_on_success_match = "required output marker"
disable_on_success_timeout_secs = 120
disable_on_success_working_dir = "/path"

Before running the normal cron prompt, execute the completion command when configured. Treat the goal as complete only if the command exits 0 and combined stdout/stderr contains the configured marker. If complete, update $HOME/.openab/cronjob.toml for the matching stable id with enabled = false and skip the normal scheduled run.

Persist scheduler writebacks atomically where possible, avoid affecting non-usercron config, and add focused tests for success, missing marker, nonzero exit, timeout, missing id, and TOML writeback behavior.

Merge Pitch

This is worth advancing because it closes a real scheduler lifecycle gap: goal-driven jobs need a first-class way to stop themselves after success.

Risk profile is moderate. The behavior touches scheduler execution and config persistence, so reviewer concern will likely center on writeback safety, race conditions, TOML preservation, and whether shell-command success checks are too footgun-prone. The PR is directionally useful, but the large src/cron.rs delta needs careful review before merge.

Best-Practice Comparison

OpenClaw principles that apply:

  • Gateway-owned scheduling: relevant. The scheduler should own the decision to skip or disable completed jobs.
  • Durable job persistence: relevant. Writing enabled = false back to cron state matches this principle.
  • Isolated executions: relevant. The completion command should have timeout, cwd handling, bounded output, and no shared mutable execution state.
  • Explicit delivery routing: partly relevant. Persisting thread_id supports durable routing, but this PR is not mainly about message delivery.
  • Retry/backoff and run logs: relevant follow-up. Completion checks should be observable when they fail, timeout, or disable a job.

Hermes Agent principles that apply:

  • Gateway daemon tick model: relevant. Completion checks belong in the scheduler tick before normal prompt execution.
  • File locking to prevent overlap: highly relevant. Writebacks to $HOME/.openab/cronjob.toml should not race with another scheduler tick or process.
  • Atomic writes for persisted state: highly relevant. Direct TOML mutation without atomic write semantics is fragile.
  • Fresh session per scheduled run: not central, except that a completed job should avoid creating a fresh run.
  • Self-contained prompts for scheduled tasks: partly relevant. The completion command should not replace the actual scheduled prompt; it should only gate whether the prompt still needs to run.

Implementation Options

Conservative option: merge only the config fields, completion check, and skip behavior, but do not write back enabled = false yet. Log that the goal completed and require the operator to disable it manually. Fastest and lowest persistence risk, but weaker user impact.

Balanced option: keep the PR’s core behavior, but harden writeback. Require stable id, use file locking plus atomic write, preserve unrelated TOML content as much as the chosen parser allows, and add targeted tests for writeback safety. This is the best fit if the scheduler already owns usercron state.

Ambitious option: introduce a durable scheduler state layer separate from the user-authored TOML. Store job completion, thread routing, run history, retry state, and disable reasons in a scheduler-owned state file or database. This aligns better with OpenClaw/Hermes long-term patterns but is larger than this PR.

Comparison Table

Option Speed to ship Complexity Reliability Maintainability User impact Fit for OpenAB right now
Conservative: check and log only High Low Medium High Medium Good if writeback risk is too high
Balanced: auto-disable with locked atomic writeback Medium Medium High High High Best
Ambitious: separate durable scheduler state Low High Very high Medium-high High Good future direction, too broad for this PR

Recommendation

Advance the balanced path.

The feature solves a concrete lifecycle problem and matches the direction of gateway-owned scheduling, but merge discussion should focus on making persistence boring: stable id required, atomic writeback, file locking, clear logs, and focused failure tests.

If the current PR does not already guarantee safe writeback semantics, split that hardening into the required follow-up before merge rather than treating it as optional polish.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

Copy link
Copy Markdown
Collaborator Author

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

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

Blocking note before merge: the timeout path around disable_on_success does not actually guarantee the spawned command is terminated. Tokio process handles continue running after drop unless kill_on_drop is enabled or the child is explicitly killed/reaped. In check_disable_on_success, timeout(child.output()) drops the output future on timeout and returns NotAchieved, but a long-running command may keep executing in the background. That violates the documented runaway-command mitigation and can leave repeated goal checks piling up. Please switch to explicit spawn + timeout around wait/output with kill/reap on timeout, or set kill_on_drop(true) before output and add a regression test using a long sleep.

@chaodu-agent

This comment has been minimized.

1 similar comment
@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

@chaodu-agent
Copy link
Copy Markdown
Collaborator Author

LGTM ✅ — Clean implementation of goal-driven auto-disable for usercron jobs. CI green, well-tested, ready to merge.

四問框架 Review

1. What problem does it solve?

Usercron jobs fire indefinitely until manually disabled. This PR adds a "stop condition" — before sending the scheduled prompt, run a command; if it exits 0 AND prints a configured marker, the goal is achieved and the job auto-disables. This enables "escape room" mode where agents work autonomously until an objective is met.

2. How does it solve it?

  • Adds disable_on_success, disable_on_success_match, disable_on_success_timeout_secs, disable_on_success_working_dir fields to CronJobConfig
  • check_disable_on_success() spawns the command, drains stdout/stderr concurrently (prevents pipe deadlock), enforces timeout with child.kill(), checks exit code + marker substring
  • update_usercron_job() uses toml_edit for surgical TOML modification (preserves formatting), atomic write via temp+rename
  • usercron_write_lock (Arc) serializes concurrent writebacks so multiple jobs don't corrupt the file
  • Validation: disable_on_success rejected in baseline [[cron.jobs]] (startup error), requires id + disable_on_success_match in usercron (skip with warning)
  • In-flight tracking no longer clears usercron indices on reload (correct — writeback changes mtime, clearing would allow overlap)

3. What was considered?

  • Usercron-only constraint (no separate state file) — correct, keeps it simple
  • Explicit marker requirement (not just exit 0) — prevents false positives from commands that succeed for unrelated reasons
  • Thread ID persistence on first thread creation — enables stable thread across restarts
  • Atomic write pattern — prevents corruption on crash

4. Is this the best approach?

Yes for Phase 1. The design is minimal and correct:

  • No new dependencies beyond toml_edit (already transitively present via toml)
  • Concurrent pipe draining prevents deadlock on large output
  • Timeout kills child to prevent orphans
  • Write serialization prevents race conditions
Traffic Light

🟢 INFO — Pipe draining via separate tokio tasks before child.wait() is the correct pattern to avoid deadlock when stdout/stderr buffers fill up. Good.

🟢 INFO — The decision to NOT clear in-flight indices on usercron reload is correct. A scheduler writeback (thread_id or enabled=false) changes mtime; clearing indices would allow the same job to fire concurrently while its previous run is still active.

🟢 INFOnon_empty_opt() helper is a nice touch — handles both None and whitespace-only strings uniformly.

🟢 INFO — Test coverage is thorough: validation tests, writeback tests, success/failure/timeout async tests, and the existing validate_cronjobs tests updated with new fields.

@chaodu-agent chaodu-agent changed the title feat(cron): auto-disable usercron jobs on success feat(cron): goal-driven auto-disable usercron jobs (implements #816) May 21, 2026
@thepagent thepagent merged commit d81442d into main May 21, 2026
16 checks passed
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.

3 participants