Files
CherryHQ-cherry-studio/docs/references/cherryclaw/scheduler.md
fullex 15fb07a3c0 chore(agents-migration): remove dead v1 agents-db drizzle migrations
The resources/database/drizzle SQL files and meta snapshots are v1-era CherryClaw agents-db migrations with no runtime consumer — the active v2 migrations live in migrations/sqlite-drizzle and are loaded from app.database.migrations. Delete the directory and clean up the now-dangling references: three source comments in AgentsDbMappings.ts that cited the deleted SQL files as the v1 column-type source (the epoch-ms notes are kept inline), and a stale doc row in cherryclaw/scheduler.md pointing to a migration file that no longer exists.
2026-07-01 07:24:34 -07:00

4.3 KiB

CherryClaw Scheduler

CherryClaw's scheduler uses a nanoclaw-inspired task-based polling design. The database is the single source of truth — no in-memory timer state is needed, and the system auto-recovers after app restart.

Architecture

SchedulerService (singleton, polling loop)
  startLoop()
    → Execute tick() every 60s
      → taskService.getDueTasks()
        → SELECT * FROM scheduled_tasks WHERE status='active' AND next_run <= now()
      → For each due task, call runTask(task) (fire-and-forget)

  runTask(task)
    1. Load agent configuration
    2. Read heartbeat file, prepend to task prompt (optional)
    3. Find or create session based on context_mode
    4. sessionMessageService.createSessionMessage({ persist: true })
    5. Drain stream and wait for completion
    6. Log run to task_run_logs
    7. computeNextRun() to calculate next run time
    8. Send task completion/failure notification via channels (optional)

  stopLoop()
    → Clear timer, abort all running tasks

Schedule Types

Type schedule_value Format Description
cron Cron expression, e.g., 0 9 * * 1-5 Standard cron scheduling (using cron-parser v5)
interval Minutes, e.g., 30 Fixed interval execution
once ISO 8601 timestamp One-time task, auto-marked as completed after execution

Drift-proof Interval Calculation

computeNextRun() anchors to the previous next_run timestamp, not the current time. If multiple intervals were missed (e.g., during app shutdown), it skips past expired intervals to calculate the next future time point:

// Anchor to scheduled time to prevent cumulative drift
let next = new Date(task.next_run).getTime() + intervalMs
while (next <= now) {
  next += intervalMs
}

This ensures interval scheduling doesn't accumulate drift from task execution time or polling delays.

Context Modes

Each task can configure context_mode:

Mode Behavior
session Reuse existing session, maintaining multi-turn conversation context
isolated Create a new session each execution, no history context

When using session mode, SessionMessageService captures the SDK's session_id (from the system/init message) and persists it as agent_session_id. On the next run, it's passed as options.resume, enabling cross-execution conversation continuity.

Heartbeat File

If an agent has heartbeat_enabled: true, the scheduler reads the heartbeat file (path specified by heartbeat_file config) before task execution and prepends it as context to the task prompt:

[Heartbeat]
{heartbeat file content}

[Task]
{task prompt}

HeartbeatReader includes path traversal protection, ensuring the heartbeat file path cannot escape the workspace directory.

Consecutive Error Handling

The scheduler tracks consecutive error counts per task. After 3 consecutive failures, the task is automatically paused (status: 'paused'). The error count resets on the next successful run. This state is tracked in memory, not persisted.

Task Completion Notifications

After each task run, notifyTaskResult() sends a status message to all channels with is_notify_receiver enabled:

[Task completed] Task Name
Duration: 12s

Or on failure:

[Task failed] Task Name
Duration: 5s
Error: error message

Notifications are sent fire-and-forget, not blocking the scheduling loop.

Manual Triggering

Besides automatic scheduling, each task can be manually triggered via API or UI:

  • API: POST /v1/agents/:agentId/tasks/:taskId/run
  • UI: "Run" button in the task settings list

runTaskNow() validates the task exists and isn't already running (returns 409 for duplicates), then triggers execution in the background.

Backward Compatibility

startScheduler(agent) and stopScheduler(agentId) are preserved as no-ops for compatibility with existing agent handler code. All scheduling logic is driven by the polling loop through database state.

Key Files

File Description
src/main/services/agents/services/SchedulerService.ts Polling scheduler main logic
src/main/services/agents/services/TaskService.ts Task CRUD, getDueTasks, computeNextRun
src/main/services/agents/database/schema/tasks.schema.ts scheduled_tasks + task_run_logs table definitions