import { EventEmitter } from 'node:events'; import { DatabaseInterface } from '@happyvertical/sql'; /** * ScheduleRunner configuration */ export interface ScheduleRunnerConfig { /** Runner ID (auto-generated if not provided) */ id?: string; /** Polling interval in milliseconds (default: 60000 - 1 minute) */ pollInterval?: number; /** Maximum schedules to process per poll */ batchSize?: number; /** * @deprecated No longer used. Slot reconciliation keys on worker liveness * (the `_smrt_workers` lease), not per-job heartbeat staleness (#1474). */ staleJobThresholdMs?: number; /** * @deprecated No longer used. See {@link staleJobThresholdMs}. */ taskHeartbeatInterval?: number; } /** * ScheduleRunner events */ export interface ScheduleRunnerEvents { 'schedule:triggered': (schedule: ScheduleInfo) => void; 'schedule:error': (schedule: ScheduleInfo, error: Error) => void; 'schedule:completed': (scheduleId: string) => void; 'schedule:failed': (scheduleId: string, error: string) => void; 'runner:started': () => void; 'runner:stopped': () => void; 'runner:error': (error: Error) => void; } /** * Schedule info for events */ export interface ScheduleInfo { id: string; agentType: string; agentId: string | null; cron: string; } /** * ScheduleRunner polls for due agent schedules and creates jobs for them * * This runner works in conjunction with TaskRunner: * 1. ScheduleRunner checks for due schedules based on cron expressions * 2. When a schedule is due, it creates a SmrtJob for the agent * 3. TaskRunner picks up and executes the job * 4. On job completion/failure, call handleJobCompletion() to update the schedule * * @example * ```typescript * const scheduleRunner = new ScheduleRunner({ pollInterval: 30000 }); * await scheduleRunner.initialize(db); * await scheduleRunner.start(); * * // Wire up TaskRunner events to update schedule state * taskRunner.on('job:completed', (job) => { * const scheduleId = job.args?._scheduleId; * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, true); * }); * taskRunner.on('job:failed', (job, error) => { * const scheduleId = job.args?._scheduleId; * if (scheduleId) scheduleRunner.handleJobCompletion(scheduleId, false, error.message); * }); * * // Graceful shutdown * process.on('SIGTERM', () => scheduleRunner.stop()); * ``` */ export declare class ScheduleRunner extends EventEmitter { readonly id: string; private readonly config; private jobCollection; private workerCollection; private running; private pollTimer; private db; private logger; constructor(config?: ScheduleRunnerConfig); /** * Initialize the runner with database connection */ initialize(db: DatabaseInterface): Promise; /** * Start processing schedules */ start(): Promise; /** * Stop processing schedules */ stop(): Promise; /** * Check if runner is running */ isRunning(): boolean; /** * Handle job completion for a scheduled job. * * Call this from TaskRunner's job:completed / job:failed events * when the job has a `_scheduleId` in its args. */ handleJobCompletion(scheduleId: string, success: boolean, errorMessage?: string): Promise; /** * Start the polling loop */ private startPolling; /** * Poll for due schedules and create jobs */ private poll; /** * Reconcile stuck schedule slots against running jobs. * * This handles two failure modes: * - a running job's owning worker is no longer alive (dead/restarted) * - a schedule slot remains occupied even though no running job still exists * * Staleness keys on worker *liveness* (issue #1474), not per-job heartbeat * freshness: a job whose `worker_id` is live in this process or holds a fresh * lease in `_smrt_workers` is healthy even if its handler is holding the loop * synchronously. ScheduleRunner has no in-process active-job set, so this is * its entire correctness mechanism. */ private recoverStaleScheduleState; private getScheduleIdFromJobArgs; /** * Trigger a schedule by creating a job */ private triggerSchedule; } /** * Validate a standard 5-field cron expression: field count plus per-field * value ranges. Throws a descriptive `Error` on the first invalid field. * * Exposed so callers (and the agents package, which owns schedule creation) * can reject a bad cron at write time rather than letting an out-of-range * field silently never match (S5 audit #1402). * * @param cron - The cron expression to validate. * @returns The trimmed, whitespace-split fields when valid. */ export declare function validateCronExpression(cron: string): string[]; /** * Parse a cron expression and get the next run date. * Supports standard 5-field cron: minute hour day-of-month month day-of-week * * Limitations: * - Numeric values only (no abbreviated names like JAN, MON) * - Day-of-week accepts 0-7 where both 0 and 7 represent Sunday * * Out-of-range fields are rejected eagerly (see {@link validateCronExpression}). * * Exported for unit testing of the matching logic (not re-exported from the * package index — the public surface is unchanged). */ export declare function getNextCronDate(cron: string): Date; /** * Create a ScheduleRunner instance */ export declare function createScheduleRunner(config?: ScheduleRunnerConfig): ScheduleRunner; export default ScheduleRunner; //# sourceMappingURL=schedule-runner.d.ts.map