import { feature } from 'bun:bundle' import { getFeatureValue_CACHED_WITH_REFRESH } from '../../services/analytics/growthbook.js' import { DEFAULT_CRON_JITTER_CONFIG } from '../../utils/cronTasks.js' import { isEnvTruthy } from '../../utils/envUtils.js' const KAIROS_CRON_REFRESH_MS = 5 * 60 * 1000 export const DEFAULT_MAX_AGE_DAYS = DEFAULT_CRON_JITTER_CONFIG.recurringMaxAgeMs / (24 * 60 * 60 * 1000) /** * Unified gate for the cron scheduling system. Combines the build-time * `feature('AGENT_TRIGGERS')` flag (dead code elimination) with the runtime * `tengu_kairos_cron` GrowthBook gate on a 5-minute refresh window. * * AGENT_TRIGGERS is independently shippable from KAIROS — the cron module * graph (cronScheduler/cronTasks/cronTasksLock/cron.ts + the three tools + * /loop skill) has zero imports into src/assistant/ and no feature('KAIROS') * calls. The REPL.tsx kairosEnabled read is safe: * kairosEnabled is unconditionally in AppStateStore with default false, so * when KAIROS is off the scheduler just gets assistantMode: false. * * Called from Tool.isEnabled() (lazy, post-init) and inside useEffect / * imperative setup, never at module scope — so the disk cache has had a * chance to populate. * * The default is `true` — /loop is GA (announced in changelog). GrowthBook * is disabled for Bedrock/Vertex/Foundry and when DISABLE_TELEMETRY / * CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC are set; a `false` default would * break /loop for those users (GH #31759). The GB gate now serves purely as * a fleet-wide kill switch — flipping it to `false` stops already-running * schedulers on their next isKilled poll tick, not just new ones. * * `CLAUDE_CODE_DISABLE_CRON` is a local override that wins over GB. */ export function isKairosCronEnabled(): boolean { return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CRON) } /** * Kill switch for disk-persistent (durable) cron tasks. Narrower than * {@link isKairosCronEnabled} — flipping this off forces `durable: false` at * the call() site, leaving session-only cron (in-memory, GA) untouched. * * Defaults to `true` so Bedrock/Vertex/Foundry and DISABLE_TELEMETRY users get * durable cron. Does NOT consult CLAUDE_CODE_DISABLE_CRON (that kills the whole * scheduler via isKairosCronEnabled). */ export function isDurableCronEnabled(): boolean { return getFeatureValue_CACHED_WITH_REFRESH( 'tengu_kairos_cron_durable', true, KAIROS_CRON_REFRESH_MS, ) } export const CRON_CREATE_TOOL_NAME = 'CronCreate' export const CRON_DELETE_TOOL_NAME = 'CronDelete' export const CRON_LIST_TOOL_NAME = 'CronList' export function buildCronCreateDescription(durableEnabled: boolean): string { return durableEnabled ? 'Schedule a prompt to run at a future time — either recurring on a cron schedule, or once at a specific time. Pass durable: true to persist to .claude/scheduled_tasks.json; otherwise session-only.' : 'Schedule a prompt to run at a future time within this Claude session — either recurring on a cron schedule, or once at a specific time.' } export function buildCronCreatePrompt(durableEnabled: boolean): string { const durabilitySection = durableEnabled ? `## Durability By default (durable: false) the job lives only in this Claude session — nothing is written to disk, and the job is gone when Claude exits. Pass durable: true to write to .claude/scheduled_tasks.json so the job survives restarts. Only use durable: true when the user explicitly asks for the task to persist ("keep doing this every day", "set this up permanently"). Most "remind me in 5 minutes" / "check back in an hour" requests should stay session-only.` : `## Session-only Jobs live only in this Claude session — nothing is written to disk, and the job is gone when Claude exits.` const durableRuntimeNote = durableEnabled ? 'Durable jobs persist to .claude/scheduled_tasks.json and survive session restarts — on next launch they resume automatically. One-shot durable tasks that were missed while the REPL was closed are surfaced for catch-up. Session-only jobs die with the process. ' : '' return `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders. Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed. ## One-shot tasks (recurring: false) For "remind me at X" or "at