Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
toml_edit = "0.22"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "model", "rustls_backend", "cache"] }
Expand Down
27 changes: 27 additions & 0 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,33 @@ timezone = "UTC"

The external `cronjob.toml` uses `[[jobs]]` (same fields). See [Usercron docs](cronjob.md#usercron--hot-reload-with-cronjobtoml) for details.

### Usercron-only `[[jobs]]` fields

These fields are valid only in the external usercron file, for example `$HOME/.openab/cronjob.toml`. They are rejected in baseline `[[cron.jobs]]` because OpenAB only writes state back to the user-managed cron file.

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `id` | string | *required with `disable_on_success`* | Stable job ID used when the scheduler writes `enabled = false` or `thread_id` back to `cronjob.toml`. |
| `disable_on_success` | string | — | Command to run before sending the scheduled prompt. |
| `disable_on_success_match` | string | *required with `disable_on_success`* | Marker that must appear in stdout or stderr, in addition to exit code `0`, before the job is considered complete. |
| `disable_on_success_timeout_secs` | integer | `60` | Timeout for the completion check command. |
| `disable_on_success_working_dir` | string | — | Working directory for the completion check command. |

Example:

```toml
[[jobs]]
id = "fix-unit-tests"
enabled = true
schedule = "*/10 * * * *"
channel = "123456789"
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"
```

**Cron expression format:**

```
Expand Down
30 changes: 29 additions & 1 deletion docs/cronjob.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,33 @@ Agent: ✅ Written to cronjob.toml, takes effect within 1 minute

This enables mobile-friendly schedule management — talk to your agent from your phone, and it updates the cron file for you.

### Goal-Driven Auto-Disable

Usercron jobs can stop themselves once a goal is complete. Add `disable_on_success` to run a command before the scheduled prompt is sent. The job is considered complete only when the command exits `0` **and** stdout or stderr contains `disable_on_success_match`.

```toml
[[jobs]]
id = "fix-unit-tests" # required for scheduler writeback
enabled = true
schedule = "*/10 * * * *"
channel = "1490282656913559673"
message = "Unit tests are still failing. Continue fixing them and report progress."

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:

1. The schedule matches.
2. The scheduler runs `disable_on_success`.
3. If the command exits `0` and output contains `disable_on_success_match`, OpenAB posts `✅ Goal achieved`, writes `enabled = false` back to `$HOME/.openab/cronjob.toml`, and skips the regular prompt.
4. Otherwise, OpenAB sends the regular `message` and the agent continues working.

`disable_on_success` is supported only in usercron `[[jobs]]`, not baseline `[[cron.jobs]]`. This keeps scheduler writeback limited to the user-managed cron file.

### Kubernetes Deployment

Mount `cronjob.toml` on a PVC so it persists across pod restarts, and set `usercron_path` in your config.toml:
Expand All @@ -273,7 +300,7 @@ usercron_path = "cronjob.toml"
- **Minute-aligned**: The scheduler aligns to minute boundaries (`:00`), so `0 9 * * *` fires at exactly 09:00:00, not at whatever second the process started.
- **Overlap protection**: If a previous execution of the same job is still running, the next tick is skipped.
- **Isolation**: Cron failures are logged but never block interactive chat traffic.
- **Stateless**: No persistence needed. Schedules are re-evaluated from config on restart.
- **Usercron persistence**: For usercron jobs, the scheduler may write `thread_id` and `enabled = false` back to `cronjob.toml`.
- **Graceful shutdown**: In-flight cron tasks are waited on (up to 30 seconds) during shutdown.

## Sender Identity
Expand Down Expand Up @@ -320,3 +347,4 @@ See [Kubernetes CronJob Reference Architecture](cronjob_k8s_refarch.md) for the
| Channel not found | Bot not in channel | Invite the bot to the target channel |
| Usercron not reloading | File not saved / wrong path | Check logs for `usercron file changed, reloading` |
| Usercron parse error | Invalid TOML syntax | Check logs for `failed to parse usercron file` |
| Goal job does not auto-disable | Command did not exit `0` or output did not include `disable_on_success_match` | Run the command manually and confirm both conditions |
16 changes: 16 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ pub struct PoolConfig {

#[derive(Debug, Clone, Deserialize)]
pub struct CronJobConfig {
/// Stable ID for usercron jobs that need scheduler writeback.
pub id: Option<String>,
/// Whether this cronjob is active (default: true)
#[serde(default = "default_true")]
pub enabled: bool,
Expand All @@ -356,6 +358,17 @@ pub struct CronJobConfig {
/// Timezone (default: "UTC")
#[serde(default = "default_cron_timezone")]
pub timezone: String,
/// Usercron-only: command to run before firing. Exit 0 plus a matching
/// `disable_on_success_match` means the goal is complete and the scheduler
/// disables the job in the usercron file.
pub disable_on_success: Option<String>,
/// Usercron-only: required output marker for `disable_on_success`.
pub disable_on_success_match: Option<String>,
/// Usercron-only: timeout for `disable_on_success`.
#[serde(default = "default_disable_on_success_timeout_secs")]
pub disable_on_success_timeout_secs: u64,
/// Usercron-only: working directory for `disable_on_success`.
pub disable_on_success_working_dir: Option<String>,
}

fn default_cron_platform() -> String {
Expand All @@ -367,6 +380,9 @@ fn default_cron_sender() -> String {
fn default_cron_timezone() -> String {
"UTC".into()
}
fn default_disable_on_success_timeout_secs() -> u64 {
60
}

/// Controls how tool calls are rendered in chat messages.
///
Expand Down
Loading
Loading