/** * [WHO]: CronTask, add/get/removeSessionCronTasks, getCronFilePath, scheduled-task storage primitives * [FROM]: Depends on node:crypto, node:fs, node:fs/promises, node:path, ./cron-parser * [TO]: Consumed by ./cron-scheduler, ./index * [HERE]: extensions/builtin/loop/cron/cron-tasks.ts - scheduled-prompt storage under /cron/scheduled_tasks.json * * Scheduled prompts, stored under the agent's config dir * (e.g. ~/.catui/agents/default/cron/scheduled_tasks.json). Agent state lives * with the agent, not in the project tree — same principle as token-save * runtime data. * * Tasks come in two flavors: * - One-shot (recurring: false/undefined) — fire once, then auto-delete. * - Recurring (recurring: true) — fire on schedule, reschedule from now, * persist until explicitly deleted via CronDelete or auto-expire after * a configurable limit (DEFAULT_CRON_JITTER_CONFIG.recurringMaxAgeMs). * * File format: * { "tasks": [{ id, cron, prompt, createdAt, recurring?, permanent? }] } */ export declare function addSessionCronTask(task: CronTask): void; export declare function getSessionCronTasks(): CronTask[]; /** * Remove session-only tasks by ID. Returns count of actually removed tasks. */ export declare function removeSessionCronTasks(ids: string[]): number; export type CronTask = { id: string; /** 5-field cron string (local time) — validated on write, re-validated on read. */ cron: string; /** Prompt to enqueue when the task fires. */ prompt: string; /** Epoch ms when the task was created. Anchor for missed-task detection. */ createdAt: number; /** * Epoch ms of the most recent fire. Written back by the scheduler after * each recurring fire so next-fire computation survives process restarts. * The scheduler anchors first-sight from `lastFiredAt ?? createdAt` — a * never-fired task uses createdAt (correct for pinned crons like * `30 14 27 2 *` whose next-from-now is next year); a fired-before task * reconstructs the same `nextFireAt` the prior process had in memory. * Never set for one-shots (they're deleted on fire). */ lastFiredAt?: number; /** When true, the task reschedules after firing instead of being deleted. */ recurring?: boolean; /** * When true, the task is exempt from recurringMaxAgeMs auto-expiry. * System escape hatch for assistant mode's built-in tasks. */ permanent?: boolean; /** * Runtime-only flag. false → session-scoped (never written to disk). * File-backed tasks leave this undefined; writeCronTasks strips it so * the on-disk shape stays { id, cron, prompt, createdAt, lastFiredAt?, recurring?, permanent? }. */ durable?: boolean; /** * Runtime-only. When set, the task was created by an in-process teammate. * The scheduler routes fires to that teammate's queue instead of the main * REPL's. Never written to disk (teammate crons are always session-only). */ agentId?: string; }; /** * Path to the cron file. `dir` must be provided explicitly (typically the * agent config dir, e.g. ~/.catui/agents/default). */ export declare function getCronFilePath(dir: string): string; /** * Read and parse the cron file. Returns an empty task list if the file * is missing, empty, or malformed. Tasks with invalid cron strings are * silently dropped (logged at debug level) so a single bad entry never * blocks the whole file. */ export declare function readCronTasks(dir: string): Promise; /** * Sync check for whether the cron file has any valid tasks. Used by * cronScheduler.start() to decide whether to auto-enable. One file read. */ export declare function hasCronTasksSync(dir: string): boolean; /** * Overwrite the cron file with the given tasks. Creates the `cron/` subdir * under `dir` if missing. Empty task list writes an empty file (rather than * deleting) so the file watcher sees a change event on last-task-removed. */ export declare function writeCronTasks(tasks: CronTask[], dir: string): Promise; /** * Append a task. Returns the generated id. Caller is responsible for having * already validated the cron string (the tool does this via validateInput). * * When `durable` is false the task is held in process memory only * (sessionTasks Map) — it fires on schedule this session but is never * written to disk and dies with the process. The scheduler merges session * tasks into its tick loop directly, so no file change event is needed. */ export declare function addCronTask(cron: string, prompt: string, recurring: boolean, durable: boolean, dir?: string, agentId?: string): Promise; /** * Remove tasks by id. No-op if none match (e.g. another session raced us). * Used for both fire-once cleanup and explicit CronDelete. * * When called with `dir` undefined (REPL path), also sweeps the in-memory * session store — the caller doesn't know which store an id lives in. * Daemon callers pass `dir` explicitly; they have no session, and the * `dir !== undefined` guard keeps this function from touching the session * store on that path. */ export declare function removeCronTasks(ids: string[], dir?: string): Promise; /** * Stamp `lastFiredAt` on the given recurring tasks and write back. Batched * so N fires in one scheduler tick = one read-modify-write, not N. Only * touches file-backed tasks — session tasks die with the process, no point * persisting their fire time. */ export declare function markCronTasksFired(ids: string[], firedAt: number, dir?: string): Promise; /** * File-backed tasks + session-only tasks, merged. Session tasks get * `durable: false` so callers can distinguish them. File tasks are * returned as-is (durable undefined → truthy). * * In Catui, always merges session tasks since the extension API * always provides `dir` (no daemon/REPL distinction like CC). */ export declare function listAllCronTasks(dir?: string): Promise; /** * Next fire time in epoch ms for a cron string, strictly after `fromMs`. * Returns null if invalid or no match in the next 366 days. */ export declare function nextCronRunMs(cron: string, fromMs: number): number | null; /** * Cron scheduler tuning knobs. Sourced at runtime from config so ops can * adjust behavior fleet-wide without shipping a client build. * Defaults here preserve the pre-config behavior exactly. */ export type CronJitterConfig = { /** Recurring-task forward delay as a fraction of the interval between fires. */ recurringFrac: number; /** Upper bound on recurring forward delay regardless of interval length. */ recurringCapMs: number; /** One-shot backward lead: maximum ms a task may fire early. */ oneShotMaxMs: number; /** * One-shot backward lead: minimum ms a task fires early when the minute-mod * gate matches. 0 = taskIds hashing near zero fire on the exact mark. Raise * this to guarantee nobody lands on the wall-clock boundary. */ oneShotFloorMs: number; /** * Jitter fires landing on minutes where `minute % N === 0`. 30 → :00/:30 * (the human-rounding hotspots). 15 → :00/:15/:30/:45. 1 → every minute. */ oneShotMinuteMod: number; /** * Recurring tasks auto-expire this many ms after creation (unless marked * `permanent`). The default (7 days) covers "check my PRs every hour this * week" workflows while capping worst-case session lifetime. * * `0` = unlimited (tasks never auto-expire). */ recurringMaxAgeMs: number; }; export declare const DEFAULT_CRON_JITTER_CONFIG: CronJitterConfig; /** * Same as {@link nextCronRunMs}, plus a deterministic per-task delay to * avoid a thundering herd when many sessions schedule the same cron string * (e.g. `0 * * * *` → everyone hits inference at :00). * * The delay is proportional to the current gap between fires * ({@link CronJitterConfig.recurringFrac}, capped at * {@link CronJitterConfig.recurringCapMs}) so at defaults an hourly task * spreads across [:00, :06) but a per-minute task only spreads by a few * seconds. * * Only used for recurring tasks. One-shot tasks use * {@link oneShotJitteredNextCronRunMs} (backward jitter, minute-gated). */ export declare function jitteredNextCronRunMs(cron: string, fromMs: number, taskId: string, cfg?: CronJitterConfig): number | null; /** * Same as {@link nextCronRunMs}, minus a deterministic per-task lead time * when the fire time lands on a minute boundary matching * {@link CronJitterConfig.oneShotMinuteMod}. * * One-shot tasks are user-pinned ("remind me at 3pm") so delaying them * breaks the contract — but firing slightly early is invisible and spreads * the inference spike from everyone picking the same round wall-clock time. * At defaults (mod 30, max 90 s, floor 0) only :00 and :30 get jitter, * because humans round to the half-hour. * * Checks the computed fire time rather than the cron string so * `0 15 * * *`, step expressions, and `0,30 9 * * *` all get jitter * when they land on a matching minute. Clamped to `fromMs` so a task created * inside its own jitter window doesn't fire before it was created. */ export declare function oneShotJitteredNextCronRunMs(cron: string, fromMs: number, taskId: string, cfg?: CronJitterConfig): number | null; /** * A task is "missed" when its next scheduled run (computed from createdAt) * is in the past. Surfaced to the user at startup. Works for both one-shot * and recurring tasks — a recurring task whose window passed while Claude * was down is still "missed". */ export declare function findMissedTasks(tasks: CronTask[], nowMs: number): CronTask[];