mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 23:33:14 +08:00
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.
119 lines
4.3 KiB
Markdown
119 lines
4.3 KiB
Markdown
# 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:
|
|
|
|
```typescript
|
|
// 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 |
|