use cm_domain::AgentId; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; /// A scheduled task an agent runs on a cron cadence (§7.6). #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Routine { pub id: Uuid, pub agent_id: Uuid, pub name: String, pub schedule_cron: String, /// `{"message": "..."}` — the prompt sent into the routine's session. pub action: serde_json::Value, pub status: String, #[serde(with = "time::serde::rfc3339::option")] pub next_run_at: Option, #[serde(with = "time::serde::rfc3339::option")] pub last_run_at: Option, } pub async fn create( pool: &PgPool, agent_id: AgentId, name: &str, schedule_cron: &str, action: serde_json::Value, next_run_at: OffsetDateTime, ) -> Result { let id = Uuid::now_v7(); sqlx::query!( "INSERT INTO routines (id, agent_id, name, schedule_cron, action, next_run_at) VALUES ($1, $2, $3, $4, $5, $6)", id, agent_id.as_uuid(), name, schedule_cron, action, next_run_at, ) .execute(pool) .await?; Ok(Routine { id, agent_id: agent_id.as_uuid(), name: name.to_owned(), schedule_cron: schedule_cron.to_owned(), action, status: "active".into(), next_run_at: Some(next_run_at), last_run_at: None, }) } pub async fn list_by_agent(pool: &PgPool, agent_id: AgentId) -> Result, DbError> { let rows = sqlx::query_as!( Routine, r#"SELECT id, agent_id, name, schedule_cron, action, status, next_run_at, last_run_at FROM routines WHERE agent_id = $1 ORDER BY created_at"#, agent_id.as_uuid(), ) .fetch_all(pool) .await?; Ok(rows) } /// Claims every due routine atomically (SKIP LOCKED: one firing per /// routine even with multiple scheduler replicas) and advances its clock. /// `next_runs` computes the following occurrence per claimed routine. pub async fn claim_due(pool: &PgPool, now: OffsetDateTime) -> Result, DbError> { let rows = sqlx::query_as!( Routine, r#"UPDATE routines SET last_run_at = $1 WHERE id IN ( SELECT id FROM routines WHERE status = 'active' AND next_run_at IS NOT NULL AND next_run_at <= $1 FOR UPDATE SKIP LOCKED ) RETURNING id, agent_id, name, schedule_cron, action, status, next_run_at, last_run_at"#, now, ) .fetch_all(pool) .await?; Ok(rows) } /// Schedules the next firing after a claim. pub async fn set_next_run( pool: &PgPool, id: Uuid, next_run_at: Option, ) -> Result<(), DbError> { sqlx::query!( "UPDATE routines SET next_run_at = $2 WHERE id = $1", id, next_run_at, ) .execute(pool) .await?; Ok(()) }