Планировщик

Внутренний планировщик, работающий внутри процесса TUI. Заменяет внешнюю команду creo cron. Выполняет две функции:

  1. Dreaming — каждые 30 минут обрабатывает до 5 необработанных сессий
  2. Scheduled prompts — выполняет промпты, запланированные агентом через tool schedule
  3. 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

Несколько экземпляров creo читают одну БД. Когда задача становится due:

  1. Все экземпляры получают её через GetDueTasks
  2. Каждый пытается 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 < ?))
  1. 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 --> [*]
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: Остальные сессии → следующая итерация
Формат Значение Пример
NULL / "" Разовое выполнение
30m, 1h, 6h Интервал «каждые 30 минут»
daily:14:00 Ежедневно в время «каждый день в 14:00»
weekly:09:00:mon Еженедельно «каждый понедельник в 09:00»

После выполнения задачи с repeat_spec, планировщик вычисляет следующий scheduled_for через computeNext(spec, now).

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 только записывает/читает БД. Выполнение — ответственность планировщика, который может быть в другом экземпляре.

  1. Создаётся новая сессия (Manager.Create(cwd))
  2. Собирается полный tool registry (agent.BuildRegistry)
  3. Создаётся Agent с WithAutoApprove() (фоновая задача, без аппрувов)
  4. Agent.Run(ctx, sessionID, "", userMessage)
  5. Результаты — в истории сессии (пользователь может переключиться и посмотреть)

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)

При старте вызывается EnsureDreamTaskINSERT OR IGNORE recurring dream задачи с фиксированным ID dream-recurring. Первый запуск — через 1 минуту, далее каждые 30 минут.