// Package cron provides a lightweight cron/scheduler for recurring agent tasks. // Jobs are persisted to JSON and executed via callback to the agent runtime. // // Three schedule types are supported: // - "at": one-time execution at a specific timestamp // - "every": recurring interval (in milliseconds) // - "cron": standard cron expression (5-field, parsed by gronx) package cron import ( "crypto/rand" "encoding/hex" "time" ) // Schedule defines when a job should run. type Schedule struct { Kind string `json:"kind"` // "at", "every", or "cron" AtMS *int64 `json:"atMs,omitempty"` // absolute timestamp (for "at") EveryMS *int64 `json:"everyMs,omitempty"` // interval in milliseconds (for "every") Expr string `json:"expr,omitempty"` // cron expression (for "cron") TZ string `json:"tz,omitempty"` // timezone (reserved) } // Payload describes what a job does when triggered. type Payload struct { Kind string `json:"kind"` // "agent_turn" Message string `json:"message"` // content to process Command string `json:"command,omitempty"` // optional shell command Deliver bool `json:"deliver"` // true = direct chat, false = agent processing Channel string `json:"channel,omitempty"` // target channel (telegram, discord, etc.) To string `json:"to,omitempty"` // target chat ID / recipient } // JobState tracks runtime state for a job. type JobState struct { NextRunAtMS *int64 `json:"nextRunAtMs,omitempty"` // next scheduled execution LastRunAtMS *int64 `json:"lastRunAtMs,omitempty"` // last execution timestamp LastStatus string `json:"lastStatus,omitempty"` // "ok" or "error" LastError string `json:"lastError,omitempty"` // error message if failed } // Job represents a scheduled cron job. type Job struct { ID string `json:"id"` Name string `json:"name"` AgentID string `json:"agentId,omitempty"` Enabled bool `json:"enabled"` Schedule Schedule `json:"schedule"` Payload Payload `json:"payload"` State JobState `json:"state"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` DeleteAfterRun bool `json:"deleteAfterRun,omitempty"` } // Store is the persistent store for all cron jobs. type Store struct { Version int `json:"version"` Jobs []Job `json:"jobs"` } // JobPatch holds optional fields for updating a job. // Only non-zero/non-nil fields are applied. Matching TS CronJobPatch. type JobPatch struct { Name string `json:"name,omitempty"` AgentID *string `json:"agentId,omitempty"` Enabled *bool `json:"enabled,omitempty"` Schedule *Schedule `json:"schedule,omitempty"` Message string `json:"message,omitempty"` Deliver *bool `json:"deliver,omitempty"` Channel *string `json:"channel,omitempty"` To *string `json:"to,omitempty"` DeleteAfterRun *bool `json:"deleteAfterRun,omitempty"` } // RunLogEntry is an in-memory record of a job execution. // Matching TS CronRunLogEntry. type RunLogEntry struct { Ts int64 `json:"ts"` JobID string `json:"jobId"` Status string `json:"status,omitempty"` // "ok", "error" Error string `json:"error,omitempty"` Summary string `json:"summary,omitempty"` } // JobHandler is a callback invoked when a job fires. // Returns the execution result string and any error. type JobHandler func(job *Job) (string, error) // generateID creates a random 8-byte hex ID for a new job. func generateID() string { b := make([]byte, 8) rand.Read(b) return hex.EncodeToString(b) } // nowMS returns the current time in milliseconds. func nowMS() int64 { return time.Now().UnixMilli() }