diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0a8ae13..31acd0c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. ### Fixes -- Heartbeat: default interval now 30m with a new default prompt + HEARTBEAT.md template. +- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior. - Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327. - Docs: add missing `ui:install` setup step in the README. Thanks @hugobarauna for PR #300. - Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312. diff --git a/docs/clawd.md b/docs/clawd.md index 1c62d396b..7f4115b70 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -166,6 +166,7 @@ By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt: Set `agent.heartbeat.every: "0m"` to disable. - If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat. +- Heartbeats run full agent turns — shorter intervals burn more tokens. ```json5 { @@ -205,7 +206,7 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`). - WebChat: [WebChat](https://docs.clawd.bot/webchat) - Gateway ops: [Gateway runbook](https://docs.clawd.bot/gateway) -- Cron + wakeups: [Cron + wakeups](https://docs.clawd.bot/cron) +- Cron + wakeups: [Cron jobs](https://docs.clawd.bot/cron-jobs) - macOS menu bar companion: [Clawdbot macOS app](https://docs.clawd.bot/macos) - iOS node app: [iOS app](https://docs.clawd.bot/ios) - Android node app: [Android app](https://docs.clawd.bot/android) diff --git a/docs/configuration.md b/docs/configuration.md index 8fc8a76cd..ea954c48b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -791,11 +791,11 @@ Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require - `model`: optional override model for heartbeat runs (`provider/model`). - `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`. - `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram). -- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). +- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md if exists` line if you still want the file read. - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). -Heartbeats run full agent turns. Shorter intervals burn more tokens; adjust `every` -and/or `model` accordingly. +Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful +of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`. `agent.bash` configures background bash defaults: - `backgroundMs`: time before auto-background (ms, default 10000) @@ -1482,7 +1482,7 @@ Template placeholders are expanded in `routing.transcribeAudio.command` (and any ## Cron (Gateway scheduler) -Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wakeups](https://docs.clawd.bot/cron) for the full RFC and CLI examples. +Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs](https://docs.clawd.bot/cron-jobs) for the feature overview and CLI examples. ```json5 { diff --git a/docs/cron-jobs.md b/docs/cron-jobs.md new file mode 100644 index 000000000..a4ad5d5c8 --- /dev/null +++ b/docs/cron-jobs.md @@ -0,0 +1,132 @@ +--- +summary: "Cron jobs + wakeups for the Gateway scheduler" +read_when: + - Scheduling background jobs or wakeups + - Wiring automation that should run with or alongside heartbeats +--- +# Cron jobs (Gateway scheduler) + +Cron runs inside the Gateway and schedules background work so Clawdbot can +wake itself up, run isolated agent jobs, and deliver reminders on time. + +## Update checklist (internal) +- [x] Audit cron + heartbeat behavior in code +- [x] Rewrite cron doc as user-facing feature +- [x] Update heartbeat docs + templates +- [x] Update cron links in docs +- [x] Update changelog +- [x] Run full gate (lint/build/test/docs) + +## What cron is +- **Gateway-owned scheduler** that persists jobs under `~/.clawdbot/cron/`. +- **Two execution modes**: + - **Main session jobs** enqueue `System:` events and rely on the heartbeat runner. + - **Isolated jobs** run a dedicated agent turn in `cron:` sessions. +- **Wakeups** are first-class: a job can trigger the next heartbeat or run it now. + +## When to use it +- Recurring reminders: “every weekday at 7:30” or “every 2h.” +- Background chores: summarize inboxes, check dashboards, watch logs. +- Automation that should not pollute the main chat history. +- Scheduled wakeups that drive the heartbeat pipeline. + +## Schedules +Cron supports three schedule kinds: +- `at`: one-shot timestamp in ms. +- `every`: fixed interval (ms). +- `cron`: 5-field cron expression, optional IANA timezone. + +Cron expressions use `croner` under the hood. If a timezone is omitted, the +server’s local timezone is used. + +## Job types + +### Main session jobs +Main jobs enqueue a system event and optionally wake the heartbeat runner. +They **must** use `payload.kind = "systemEvent"`. + +- **`wakeMode: "next-heartbeat"`** (default): the event waits for the next + scheduled heartbeat. +- **`wakeMode: "now"`**: the event triggers an immediate heartbeat run. + +### Isolated jobs +Isolated jobs run a dedicated agent turn in session `cron:` and can +optionally deliver a message. + +Key behaviors: +- Prompt is prefixed with `[cron: ]` for traceability. +- A summary is posted to the main session with prefix `Cron` (or + `isolation.postToMainPrefix`). +- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary. +- `payload.deliver: true` sends output to a provider; otherwise it stays internal. + +## Storage & history +- Job store: `~/.clawdbot/cron/jobs.json` (JSON, Gateway-managed). +- Run history: `~/.clawdbot/cron/runs/.jsonl` (JSONL, auto-pruned). +- Override store path: `cron.store` in config. + +## Configuration + +```json5 +{ + cron: { + enabled: true, // default true + store: "~/.clawdbot/cron/jobs.json", + maxConcurrentRuns: 1 // default 1 + } +} +``` + +Disable cron entirely: +- `cron.enabled: false` (config) +- or `CLAWDBOT_SKIP_CRON=1` (env) + +## CLI quickstart + +One-shot reminder (main session, wake immediately): +```bash +clawdbot cron add \ + --name "Calendar check" \ + --at "20m" \ + --session main \ + --system-event "Next heartbeat: check calendar." \ + --wake now +``` + +Recurring isolated job (deliver to WhatsApp): +```bash +clawdbot cron add \ + --name "Morning status" \ + --cron "0 7 * * *" \ + --tz "America/Los_Angeles" \ + --session isolated \ + --message "Summarize inbox + calendar for today." \ + --deliver \ + --provider whatsapp \ + --to "+15551234567" +``` + +Manual run (debug): +```bash +clawdbot cron run --force +``` + +Run history: +```bash +clawdbot cron runs --id --limit 50 +``` + +Immediate wake without creating a job: +```bash +clawdbot wake --mode now --text "Next heartbeat: check battery." +``` + +## API surface (Gateway) +- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove` +- `cron.run` (force or due), `cron.runs` +- `wake` (enqueue system event + optional heartbeat) + +## Tips +- Use **main session jobs** when you want the heartbeat prompt + existing context. +- Use **isolated jobs** for noisy, frequent, or long-running work. +- Keep messages short; cron turns are full agent runs and can burn tokens. diff --git a/docs/cron.md b/docs/cron.md deleted file mode 100644 index 5c76c8b8d..000000000 --- a/docs/cron.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -summary: "RFC: Cron jobs + wakeups for Clawd/Clawdbot (main vs isolated sessions)" -read_when: - - Designing scheduled jobs, alarms, or wakeups - - Adding Gateway methods or CLI commands for automation - - Adjusting heartbeat behavior or session routing ---- - -# RFC: Cron jobs + wakeups for Clawd - -Status: Draft -Last updated: 2025-12-13 - -## Context - -Clawdbot already has: -- A **gateway heartbeat runner** that runs the agent with the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) and suppresses `HEARTBEAT_OK` ([`src/infra/heartbeat-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-runner.ts)). -- A lightweight, in-memory **system event queue** (`enqueueSystemEvent`) that is injected into the next **main session** turn (`drainSystemEvents` in [`src/auto-reply/reply.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/auto-reply/reply.ts)). -- A WebSocket **Gateway** daemon that is intended to be always-on ([`docs/gateway.md`](https://docs.clawd.bot/gateway)). - -This RFC adds a small “cron job system” so Clawd can schedule future work and reliably wake itself up: -- **Delayed**: run on the *next* normal heartbeat tick -- **Immediate**: run *now* (trigger a heartbeat immediately) -- **Isolated jobs**: optionally run in their own session that does not pollute the main session and can run concurrently (within configured limits). - -## Goals - -- Provide a **persistent job store** and an **in-process scheduler** owned by the Gateway. -- Allow each job to target either: - - `sessionTarget: "main"`: inject as `System:` lines and rely on the main heartbeat (or trigger it immediately). - - `sessionTarget: "isolated"`: run an agent turn in a dedicated session key (job session), optionally delivering a message and/or posting a summary back to main. -- Expose a stable control surface: - - **Gateway methods** (`cron.*`, `wake`) for programmatic usage (mac app, CLI, agents). - - **CLI commands** (`clawdbot cron ...`) to add/remove/edit/list and to debug `run`. -- Produce clear, structured **logs** for job lifecycle and execution outcomes. - -## Non-goals (v1) - -- Multi-host distributed scheduling. -- Exactly-once semantics across crashes (we aim for “at-least-once with idempotency hooks”). -- A full Unix-cron parser as the only schedule format (we can support it, but v1 should not require complex cron features to be useful). - -## Terminology - -- **Wake**: a request to ensure the agent gets a turn soon (either right now or next heartbeat). -- **Main session**: the canonical session bucket (default key `"main"`) that receives `System:` events. -- **Isolated session**: a per-job session key (e.g. `cron:`) with its own session id / session file. - -## User stories - -- “Remind me in 20 minutes” → add a one-shot job that triggers an immediate heartbeat at T+20m. -- “Every weekday at 7:30, wake me up and start music” → recurring job, isolated session, deliver to WhatsApp. -- “Every hour, check battery; only interrupt me if < 20%” → isolated job that decides whether to deliver; may also post a brief status to main. -- “Next heartbeat, please check calendar” → delayed wake targeting main session. - -## Job model - -### Storage schema (v1) - -Each job is a JSON object with stable keys (unknown keys ignored for forward compatibility): - -- `id: string` (UUID) -- `name: string` (required) -- `description?: string` (optional) -- `enabled: boolean` -- `createdAtMs: number` -- `updatedAtMs: number` -- `schedule` (one of) - - `{"kind":"at","atMs":number}` (one-shot) - - `{"kind":"every","everyMs":number,"anchorMs"?:number}` (simple interval) - - `{"kind":"cron","expr":string,"tz"?:string}` (optional; see “Schedule parsing”) -- `sessionTarget: "main" | "isolated"` -- `wakeMode: "next-heartbeat" | "now"` - - For `sessionTarget:"isolated"`, `wakeMode:"now"` means “run immediately when due”. - - For `sessionTarget:"main"`, `wakeMode` controls whether we trigger the heartbeat immediately or just enqueue and wait. -- `payload` (one of) - - `{"kind":"systemEvent","text":string}` (enqueue as `System:`) - - `{"kind":"agentTurn","message":string,"deliver"?:boolean,"provider"?: "last"|"whatsapp"|"telegram"|"discord"|"signal"|"imessage","to"?:string,"timeoutSeconds"?:number}` -- `isolation` (optional; only meaningful for isolated jobs) - - `{"postToMainPrefix"?: string}` -- `runtime` (optional) - - `{"maxAttempts"?:number,"retryBackoffMs"?:number}` (best-effort retries; defaults off) -- `state` (runtime-maintained) - - `{"nextRunAtMs":number,"lastRunAtMs"?:number,"lastStatus"?: "ok"|"error"|"skipped","lastError"?:string,"lastDurationMs"?:number}` - -### Key behavior - -- `sessionTarget:"main"` jobs always enqueue `payload.kind:"systemEvent"` (directly or derived from `agentTurn` results; see below). -- `sessionTarget:"isolated"` jobs create/use a stable session key: `cron:`. - -## Storage location - -Cron persists everything under `~/.clawdbot/cron/`: -- Job store: `~/.clawdbot/cron/jobs.json` -- Run history: `~/.clawdbot/cron/runs/.jsonl` - -You can override the job store path via `cron.store` in config. - -The scheduler should never require additional configuration for the base directory (Clawdbot already treats `~/.clawdbot` as fixed). - -## Enabling - -Cron execution is enabled by default inside the Gateway. - -To disable it, set: - -```json5 -{ - cron: { - enabled: false, - // optional: - store: "~/.clawdbot/cron/jobs.json", - maxConcurrentRuns: 1 - } -} -``` - -You can also disable scheduling via the environment variable `CLAWDBOT_SKIP_CRON=1`. - -## Scheduler design - -### Ownership - -The Gateway owns: -- the scheduler timer, -- job store reads/writes, -- job execution (enqueue system events and/or agent turns). - -This keeps scheduling unified with the always-on process and prevents “two schedulers” when multiple CLIs run. - -### Timer strategy - -- Maintain an in-memory heap/array of enabled jobs keyed by `state.nextRunAtMs`. -- Use a **single `setTimeout`** to wake at the earliest next run. -- On wake: - - compute all due jobs (now >= nextRunAtMs), - - mark them “in flight” (in memory), - - persist updated `state` (at least bump `nextRunAtMs` / `lastRunAtMs`) before starting execution to minimize duplicate runs on crash, - - execute jobs (with concurrency limits), - - persist final `lastStatus/lastError/lastDurationMs`, - - re-arm timer for the next earliest run. - -### Schedule parsing - -V1 can ship with `at` + `every` without extra deps. - -If we add `"kind":"cron"`: -- Use a well-maintained parser (we use `croner`) and support: - - 5-field cron (`min hour dom mon dow`) at minimum - - optional `tz` -- Store `nextRunAtMs` computed by the parser; re-compute after each run. - -## Execution semantics - -### Main session jobs - -Main session jobs do not run the agent directly by default. - -When due: -1) `enqueueSystemEvent(job.payload.text)` (or a derived message) -2) If `wakeMode:"now"`, trigger an immediate heartbeat run (see “Heartbeat wake hook”). -3) Otherwise do nothing else (the next scheduled heartbeat will pick up the system event). - -Why: This keeps the main session’s “proactive” behavior centralized in the heartbeat rules and avoids ad-hoc agent turns that might fight with inbound message processing. - -### Isolated session jobs - -Isolated jobs run an agent turn in a dedicated session key, intended to be separate from main. - -When due: -- Build a message body that includes schedule metadata, e.g.: - - `"[cron:] : "` -- Execute via the same agent runner path as other command-mode runs, but pinned to: - - `sessionKey = cron:` - - `sessionId = store[sessionKey].sessionId` (create if missing) -- Optionally deliver output (`payload.deliver === true`) to the configured provider/to. -- Isolated jobs always enqueue a summary system event to the main session when they finish (derived from the last agent text output). - - Prefix defaults to `Cron`, and can be customized via `isolation.postToMainPrefix`. -- If `deliver` is omitted/false, nothing is sent to external providers; you still get the main-session summary and can inspect the full isolated transcript in `cron:`. - -### “Run in parallel to main” - -Clawdbot currently serializes command execution through a global in-process queue ([`src/process/command-queue.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/process/command-queue.ts)) to avoid collisions. - -To support isolated cron jobs running “in parallel”, we should introduce **lanes** (keyed queues) plus a global concurrency cap: -- Lane `"main"`: inbound auto-replies + main heartbeat. -- Lane `"cron"` (or `cron:`): isolated jobs. -- Configurable `cron.maxConcurrentRuns` (default 1 or 2). - -This yields: -- isolated jobs can overlap with the main lane (up to cap), -- each lane still preserves ordering for its own work (optional), -- we retain safety knobs to prevent runaway resource contention. - -## Heartbeat wake hook (immediate vs next heartbeat) - -We need a way for the Gateway (or the scheduler) to request an immediate heartbeat without duplicating heartbeat logic. - -Design: -- `startHeartbeatRunner` owns the real heartbeat execution and installs a wake handler. -- Wake hook lives in [`src/infra/heartbeat-wake.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-wake.ts): - - `setHeartbeatWakeHandler(fn | null)` installed by the heartbeat runner - - `requestHeartbeatNow({ reason, coalesceMs? })` -- If the handler is absent, the request is stored as “pending”; the next time the handler is installed, it runs once. -- Coalesce rapid calls and respect the “skip when queue busy” behavior (retry soon vs dropping). - -## Run history log (JSONL) - -In addition to normal structured logs, the Gateway writes an append-only run history “ledger” (JSONL) whenever a job finishes. This is intended for quick debugging (“did the job run, when, and what happened?”). - -Path rules: -- Run logs are stored per job next to the store: `.../runs/.jsonl`. - -Retention: -- Best-effort pruning when the file grows beyond ~2MB; keep the newest ~2000 lines. - -Each log line includes (at minimum) job id, status/error, timing, and a `summary` string (systemEvent text for main jobs, and the last agent text output for isolated jobs). - -## Compatibility policy (cron.add/cron.update) - -To keep older clients working, the Gateway applies **best-effort normalization** for `cron.add` and `cron.update`: -- Accepts wrapped payloads under `data` or `job` and unwraps them. -- Infers `schedule.kind` from `atMs`, `everyMs`, or `expr` if missing. -- Infers `payload.kind` from `text` (systemEvent) or `message` (agentTurn) if missing. -- Defaults `wakeMode` to `"next-heartbeat"` when omitted. -- Defaults `sessionTarget` based on payload kind (`systemEvent` → `"main"`, `agentTurn` → `"isolated"`). - -Normalization is **compat-only**. New clients should send the full schema (including `kind`, `sessionTarget`, and `wakeMode`) to avoid ambiguity. Unknown fields are still rejected by schema validation. - -## Gateway API - -New methods (names can be bikeshed; `cron.*` is suggested): - -- `wake` - - params: `{ mode: "now" | "next-heartbeat", text: string }` - - effect: `enqueueSystemEvent(text)`, plus optional immediate heartbeat trigger - -- `cron.list` - - params: optional `{ includeDisabled?: boolean }` - - returns: `{ jobs: CronJob[] }` - -- `cron.add` - - params: job payload without `id/state` (server generates and returns created job) - -- `cron.update` - - params: `{ id: string, patch: Partial }` - -- `cron.remove` - - params: `{ id: string }` - -- `cron.run` - - params: `{ id: string, mode?: "due" | "force" }` (debugging; does not change schedule unless `force` requires it) - -- `cron.runs` - - params: `{ id: string, limit?: number }` - - returns: `{ entries: CronRunLogEntry[] }` - - note: `id` is required (runs are stored per-job). - -The Gateway should broadcast a `cron` event for UI/debug: -- event: `cron` - - payload: `{ jobId, action: "added"|"updated"|"removed"|"started"|"finished", status?, error?, nextRunAtMs? }` - -## CLI surface - -Add a `cron` command group (all commands should also support `--json` where sensible): - -- `clawdbot cron list [--json] [--all]` -- `clawdbot cron add ...` - - schedule flags: - - `--at ` (one-shot) - - `--every ` (e.g. `10m`, `1h`) - - `--cron "" [--tz ""]` - - target flags: - - `--session main|isolated` - - `--wake now|next-heartbeat` - - payload flags (choose one): - - `--system-event ""` - - `--message "" [--deliver] [--provider last|whatsapp|telegram|discord|slack|signal|imessage] [--to ]` - -- `clawdbot cron edit ...` (patch-by-flags, non-interactive) -- `clawdbot cron rm ` -- `clawdbot cron enable ` / `clawdbot cron disable ` -- `clawdbot cron run [--force]` (debug) -- `clawdbot cron runs --id [--limit ]` (run history) -- `clawdbot cron status` (scheduler enabled + next wake) - -Additionally: -- `clawdbot wake --mode now|next-heartbeat --text ""` as a thin wrapper around `wake` for agents to call. - -## Examples - -### Run once at a specific time - -One-shot reminder that targets the main session and triggers a heartbeat immediately at the scheduled time: - -```bash -clawdbot cron add \ - --at "2025-12-14T07:00:00-08:00" \ - --session main \ - --wake now \ - --system-event "Alarm: wake up (meeting in 30 minutes)." -``` - -### Run daily (calendar-accurate) - -Daily at 07:00 in a specific timezone (preferred over “every 24h” to avoid DST drift): - -```bash -clawdbot cron add \ - --cron "0 7 * * *" \ - --tz "America/Los_Angeles" \ - --session isolated \ - --wake now \ - --message "Daily check: scan calendar + inbox; deliver only if urgent." \ - --deliver \ - --provider last -``` - -### Run weekly (every Wednesday) - -Every Wednesday at 09:00: - -```bash -clawdbot cron add \ - --cron "0 9 * * 3" \ - --tz "America/Los_Angeles" \ - --session isolated \ - --wake now \ - --message "Weekly: summarize status and remind me of goals." \ - --deliver \ - --provider last -``` - -### “Next heartbeat” - -Enqueue a note for the main session but let the existing heartbeat cadence pick it up: - -```bash -clawdbot wake --mode next-heartbeat --text "Next heartbeat: check battery + upcoming meetings." -``` - -## Logging & observability - -Logging requirements: -- Use `getChildLogger({ module: "cron", jobId, runId, name })` for every run. -- Log lifecycle: - - store load/save (debug; include job count) - - schedule recompute (debug; include nextRunAt) - - job start/end (info) - - job skipped (info; include reason) - - job error (warn; include error + stack where available) -- Emit a concise user-facing line to stdout when running in CLI mode (similar to heartbeat logs). - -Suggested log events: -- `cron: scheduler started` (jobCount, nextWakeAt) -- `cron: job started` (jobId, scheduleKind, sessionTarget, wakeMode) -- `cron: job finished` (status, durationMs, nextRunAtMs) -- When `cron.enabled` is false, the Gateway logs `cron: disabled` and jobs will not run automatically (the CLI warns on `cron add`/`cron edit`). -- Use `clawdbot cron status` to confirm the scheduler is enabled and see the next wake time. - -## Safety & security - -- Respect existing allowlists/routing rules: delivery defaults should not send to arbitrary destinations unless explicitly configured. -- Provide a global “kill switch”: - - `cron.enabled: boolean` (default `true`). - - `gateway method set-heartbeats` already exists; cron should have similar. -- Avoid persistence of sensitive payloads unless requested; job text may contain private content. - -## Testing plan (v1) - -- Unit tests: - - schedule computation for `at` and `every` - - job store read/write + migration behavior - - lane concurrency: main vs cron overlap is bounded - - “wake now” coalescing and pending behavior when provider not ready -- Integration tests: - - start Gateway with `CLAWDBOT_SKIP_PROVIDERS=1`, add jobs, list/edit/remove - - simulate due jobs and assert `enqueueSystemEvent` called + cron events broadcast - -## Rollout plan - -1) Add the `wake` primitive + heartbeat wake hook (no persistent jobs yet). -2) Add `cron.*` API and CLI wrappers with `at` + `every`. -3) Add optional cron expression parsing (`kind:"cron"`) if needed. -4) Add UI surfacing in WebChat/macOS app (optional). diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 6fb021a4d..bb7b4f73b 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -8,17 +8,28 @@ read_when: Heartbeat runs periodic agent turns in the **main session** so the model can surface anything that needs attention without spamming the user. +## Defaults +- Interval: `30m` (set `agent.heartbeat.every` to change, `0m` disables). +- Prompt body (configurable via `agent.heartbeat.prompt`): + `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` +- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does + not append extra body text. The system prompt includes a Heartbeats section + and the run is flagged as a heartbeat internally. + ## Prompt contract -- Heartbeat body defaults to: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` (configurable via `agent.heartbeat.prompt`). - If nothing needs attention, the model should reply `HEARTBEAT_OK`. - During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at the **start or end** of the reply. Clawdbot strips the token and discards the reply if the remaining content is **≤ `ackMaxChars`** (default: 30). - If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially. - For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text. -- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does - not append extra body text. The system prompt includes a Heartbeats section - and the run is flagged as a heartbeat internally. + +## Prompt overrides +- Overriding `agent.heartbeat.prompt` **replaces** the default body. Nothing is + merged for you. +- If you still want `HEARTBEAT.md` instructions, keep a line like + `Read HEARTBEAT.md if exists` in your custom prompt. +- `HEARTBEAT_OK` handling stays the same; changing the prompt won’t break acks. ### Stray `HEARTBEAT_OK` outside heartbeats If the model accidentally includes `HEARTBEAT_OK` at the start or end of a @@ -63,9 +74,9 @@ and final replies: - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). ## Cost awareness -Heartbeats run full agent turns. Shorter intervals burn more tokens. If you -don’t need frequent checks, increase `every`, pick a cheaper `model`, or set -`target: "none"` to keep results internal. +Heartbeats run full agent turns. Shorter intervals burn more tokens. Be +intentional about `every`, keep `HEARTBEAT.md` tiny, and consider a cheaper +`model` or `target: "none"` if you only want internal state updates. ## HEARTBEAT.md (optional) If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the @@ -85,6 +96,8 @@ bloat. - Handle mundane tasks (triage inboxes, summarize queues, refresh notes). - Nudge on open loops or reminders. - Background monitoring (health checks, status polling, low-priority alerts). +- Scheduled routines (use [Cron jobs](https://docs.clawd.bot/cron-jobs) when you + need exact schedules or isolated runs). ## Wake hook - The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an diff --git a/docs/hubs.md b/docs/hubs.md index 87e74d053..4babee171 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -80,7 +80,7 @@ Use these hubs to discover every page, including deep dives and reference docs t - [Tools surface](https://docs.clawd.bot/tools) - [Bash tool](https://docs.clawd.bot/bash) - [Elevated mode](https://docs.clawd.bot/elevated) -- [Cron + wakeups](https://docs.clawd.bot/cron) +- [Cron jobs](https://docs.clawd.bot/cron-jobs) - [Thinking + verbose](https://docs.clawd.bot/thinking) - [Models](https://docs.clawd.bot/models) - [Agent send CLI](https://docs.clawd.bot/agent-send) diff --git a/docs/index.md b/docs/index.md index 862aa4b39..f39dfcc4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -171,7 +171,7 @@ Example: - [Linux app](https://docs.clawd.bot/linux) - Ops and safety: - [Sessions](https://docs.clawd.bot/session) - - [Cron + wakeups](https://docs.clawd.bot/cron) + - [Cron jobs](https://docs.clawd.bot/cron-jobs) - [Security](https://docs.clawd.bot/security) - [Troubleshooting](https://docs.clawd.bot/troubleshooting) diff --git a/docs/plans/cron-add-hardening.md b/docs/plans/cron-add-hardening.md index 2ba67ea66..f3277e8e3 100644 --- a/docs/plans/cron-add-hardening.md +++ b/docs/plans/cron-add-hardening.md @@ -43,7 +43,7 @@ Recent gateway logs show repeated `cron.add` failures with invalid parameters (m - [x] Fix UI CronStatus type to match gateway (`jobs` instead of `jobCount`). - [x] Update cron UI provider select to include Discord/Slack/Signal/iMessage. - [x] Update macOS CronJobEditor provider picker + enum to include Slack/Signal/iMessage. -- [x] Document cron compatibility normalization policy in [`docs/cron.md`](https://docs.clawd.bot/cron). +- [x] Document cron compatibility normalization policy in [`docs/cron-jobs.md`](https://docs.clawd.bot/cron-jobs). ### Phase 2 — Input normalization + tooling hardening - [x] Add shared cron input normalization helpers (`normalizeCronJobCreate`/`normalizeCronJobPatch`). diff --git a/docs/templates/AGENTS.md b/docs/templates/AGENTS.md index 051f19c00..34dbd3465 100644 --- a/docs/templates/AGENTS.md +++ b/docs/templates/AGENTS.md @@ -120,7 +120,7 @@ When you receive a heartbeat poll (message matches the configured heartbeat prom Default heartbeat prompt: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` -You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small. +You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small to limit token burn. **Things to check (rotate through these, 2-4 times per day):** - **Emails** - Any urgent unread messages? diff --git a/docs/templates/HEARTBEAT.md b/docs/templates/HEARTBEAT.md index 4d300f421..45d86581f 100644 --- a/docs/templates/HEARTBEAT.md +++ b/docs/templates/HEARTBEAT.md @@ -5,4 +5,4 @@ read_when: --- # HEARTBEAT.md -Keep this file empty unless you want a tiny checklist for heartbeat runs. Keep it small. +Keep this file empty unless you want a tiny checklist. Keep it small. diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index a27dc7e5f..f29e97271 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -87,10 +87,9 @@ It does not define which tools exist; Clawdbot provides built-in tools internall Add whatever else you want the assistant to know about your local toolchain. `; -const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md - Optional heartbeat notes +const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md -Keep this file small. Leave it empty unless you want a short checklist or reminders -to follow during heartbeat runs. +Keep this file empty unless you want a tiny checklist. Keep it small. `; const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after)