📋 Architecture Overview
Layer 1 — Tools
CronCreate / Delete / List - model-facing API, validation, file I/O.
Layer 2 — Scheduler Core
cronScheduler.ts - 1s tick loop, chokidar file watcher, lock, jitter.
Layer 3 — Storage
.claude/scheduled_tasks.json + in-memory session store for ephemeral tasks.
Layer 4 — React Glue
useScheduledTasks - mounts scheduler in REPL, routes fires to message queue.
Layer 5 — Fleet Ops
cronJitterConfig.ts - GrowthBook-backed tuning pushed live without restart.
📝 CronTask Data Model
Each task has: id (8-hex UUID slice), cron (5-field local time), prompt, createdAt, lastFiredAt, recurring, permanent, durable, agentId. The durable flag never touches disk - writeCronTasks strips it.
💡 Two Flavors
One-shot (recurring: false): fire once, auto-delete. Recurring (recurring: true): fire, reschedule from now, auto-expire after 7 days.
🔄 Scheduler Lifecycle
Created once per REPL session. Lazy-enable design: chokidar and timers don't start until tasks exist. EnablePoll probes every 1s. Timer is unref'd so -p mode can exit.
💡 Catch-Up Prevention
Recurring tasks reschedule from 'now', not from computed fire time. If session blocked and 9am task fires at 9:05, next fire computes from 9:05.
🔒 Multi-Session Distributed Lock
Per-project scheduler lock prevents double-firing when multiple sessions share a directory. Lock liveness-probed by PID. Non-owners probe every 5 seconds to detect stale locks from crashed sessions.
💡 Session Tasks Are Lock-Exempt
Session-only tasks live in process-private memory - no shared file, no double-fire risk. Lock guard only applies to file-backed tasks.
📊 Load-Spreading Jitter
Deterministic per-task
jitterFrac = parseInt(taskId.slice(0,8), 16) / 0x1_0000_0000. Stable across restarts, uniform across fleet.
Recurring: forward jitter
Fires up to recurringFrac * interval late (cap: 15 min). Hourly with defaults: up to 6 min spread.
One-shot: backward jitter
Fires up to 90s early, only on :00/:30 minutes. Humans pick round times; firing early is invisible to user.
💡 GrowthBook Live Ops
Jitter config from tengu_kairos_cron_config feature flag. Ops can push changes during incident. Fleet converges within 60 seconds.
🚨 Missed Tasks & Startup
findMissedTasks() compares each task's first fire time from createdAt to now. Missed one-shots surfaced with injection-resistant prompt (adaptive backtick fence). Missed recurring tasks NOT surfaced - scheduler fires on first tick.
💡 Prompt Injection Defense
buildMissedTaskNotification() finds longest backtick run in prompt, opens fence with one more backtick. Prevents prompt injection via crafted backtick sequences.
🛑 Feature Gates & Kill Switches
| Gate | Type | Effect |
|---|---|---|
| feature('AGENT_TRIGGERS') | Build-time | Dead-code elimination of entire cron module |
| CLAUDE_CODE_DISABLE_CRON=1 | Env var | Local override, kills all scheduling |
| tengu_kairos_cron | GrowthBook | Fleet-wide kill switch, polled every 5 min |
| tengu_kairos_cron_durable | GrowthBook | Kills disk persistence only, session cron stays |
| tengu_kairos_cron_config | GrowthBook | Jitter tuning without deploy |