Планировщик
Внутренний планировщик, работающий внутри процесса TUI. Заменяет внешнюю
команду creo cron. Выполняет две функции:
- Dreaming — каждые 30 минут обрабатывает до 5 необработанных сессий
- Scheduled prompts — выполняет промпты, запланированные агентом через tool
schedule - Session types — сессии планировщика помечаются
session_type='scheduler'и отображаются в TUI в отдельной группе «⏰ Планировщик»
Все задачи — в единой очереди в SQLite. Несколько экземпляров creo никогда не выполнят одну и ту же задачу дважды благодаря атомарному захвату через БД.
Архитектура
graph TB
subgraph "TUI процесс (каждый экземпляр creo)"
TUI["TUI (bubbletea)"]
Sched["Scheduler goroutine<br/>тик каждые 30 сек"]
end
subgraph "SQLite (разделяется между экземплярами)"
Tasks["scheduler_tasks<br/>единая очередь"]
end
Tool["schedule tool<br/>(в tool registry)"]
Dream["dream-recurring task<br/>(type=dream, repeat=30m)"]
Tool -->|"AddScheduledTask"| Tasks
Sched -->|"GetDueTasks + ClaimTask"| Tasks
Dream --- Tasks
Sched -->|"type=dream"| DreamRun["runDream<br/>max 5 sessions<br/>→ ReviewerRunner"]
Sched -->|"type=prompt"| PromptRun["runPrompt<br/>new session + Agent.Run<br/>(autoApprove)"]
DreamRun -->|"reschedule +30m"| Tasks
PromptRun -->|"reschedule or done"| Tasks
Атомарный захват (multi-instance)
Несколько экземпляров creo читают одну БД. Когда задача становится due:
- Все экземпляры получают её через
GetDueTasks - Каждый пытается
ClaimTask— атомарныйUPDATE:
1UPDATE scheduler_tasks
2SET status='running', locked_at=?, locked_by=?
3WHERE id=?
4 AND (status='pending'
5 OR (status='running' AND locked_at < ?))
RowsAffected() == 1→ этот экземпляр забрал.0→ другой успел первым. SQLite сериализует WRITE — гонка невозможна.
Stale-восстановление: если задача висит в running > 15 минут (упал
процесс), другой экземпляр может перехватить (условие locked_at < now - 900).
Схема БД
1CREATE TABLE scheduler_tasks (
2 id TEXT PRIMARY KEY,
3 type TEXT NOT NULL DEFAULT 'prompt', -- 'prompt' | 'dream'
4 session_id TEXT, -- сессия-инициатор
5 prompt TEXT,
6 scheduled_for INTEGER NOT NULL, -- unix timestamp
7 repeat_spec TEXT, -- шаблон повтора или NULL
8 status TEXT NOT NULL DEFAULT 'pending', -- pending | running | done
9 locked_at INTEGER,
10 locked_by INTEGER,
11 created_at INTEGER NOT NULL,
12 last_error TEXT
13);
Жизненный цикл задачи
stateDiagram-v2
[*] --> pending: AddScheduledTask / EnsureDreamTask
pending --> running: ClaimTask (атомарный UPDATE)
running --> pending: RescheduleTask (repeat_spec задан)
running --> done: CompleteTask (разовая задача)
running --> running: Stale recovery (>15 мин, другой PID)
done --> [*]
Dreaming
sequenceDiagram
participant S as Scheduler
participant DB as SQLite
participant R as ReviewerRunner
Note over S: Тик → GetDueTasks → ClaimTask(dream-recurring)
S->>DB: ListAllSessions
DB-->>S: все сессии
S->>S: фильтр: root + dream_done != "1" → первые 5
loop Для каждой из 5 сессий
S->>R: Run(session)
R-->>S: результат
S->>DB: SetMeta(sessionID, "dream_done", "1")
end
S->>DB: RescheduleTask(dream-recurring, now + 30m)
Note over S: Остальные сессии → следующая итерация
Формат repeat_spec
| Формат | Значение | Пример |
|---|---|---|
NULL / "" |
Разовое выполнение | — |
30m, 1h, 6h |
Интервал | «каждые 30 минут» |
daily:14:00 |
Ежедневно в время | «каждый день в 14:00» |
weekly:09:00:mon |
Еженедельно | «каждый понедельник в 09:00» |
После выполнения задачи с repeat_spec, планировщик вычисляет следующий
scheduled_for через computeNext(spec, now).
Tool schedule
schedule(
action: "create" | "list" | "cancel", — операция
— для create:
prompt: string, — что выполнить (self-contained, без контекста текущей сессии)
when: string, — первый запуск: "30m" | "2h" | "1d" | RFC3339
repeat?: string — повторы: "1h" | "daily:14:00" | "weekly:09:00:mon"
— для cancel:
task_id: string — ID задачи для отмены
)
| Action | Что делает |
|---|---|
create |
Создаёт задачу в БД. Scheduler выполнит её в новой сессии с autoApprove. |
list |
Показывает все активные (pending/running) задачи: ID, время, repeat, промпт. |
cancel |
Отменяет задачу по ID (mark as done, больше не выполнится). |
update |
Меняет prompt/when/repeat у существующей задачи. Только переданные поля; задача сбрасывается в pending. |
Tool только записывает/читает БД. Выполнение — ответственность планировщика, который может быть в другом экземпляре.
Scheduled prompt выполнение
- Создаётся новая сессия (
Manager.Create(cwd)) - Собирается полный tool registry (
agent.BuildRegistry) - Создаётся
AgentсWithAutoApprove()(фоновая задача, без аппрувов) Agent.Run(ctx, sessionID, "", userMessage)- Результаты — в истории сессии (пользователь может переключиться и посмотреть)
Запуск
Scheduler стартует в runTUI (cmd/creo/tui.go) как фоновая горутина с
собственным context, который отменяется при выходе из TUI.
1sched := scheduler.New(scheduler.Config{...})
2schedCtx, schedCancel := context.WithCancel(context.Background())
3defer schedCancel()
4go sched.Start(schedCtx)
При старте вызывается EnsureDreamTask — INSERT OR IGNORE recurring dream
задачи с фиксированным ID dream-recurring. Первый запуск — через 1 минуту,
далее каждые 30 минут.