Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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