use std::collections::BTreeSet;
use std::sync::{Mutex, OnceLock};
use crate::utils::lock::LockRecover;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::paths::removed_default_routines_path;
use crate::routine_storage::write_routine;
use crate::utils::cron::normalize_schedule;
use crate::utils::time::now_secs;
use super::command::slugify;
use super::model::{Routine, RoutineStore};
mod the_1_percent;
mod token_trim;
mod update_moadim;
struct DefaultRoutine {
title: &'static str,
schedule: &'static str,
agent: &'static str,
prompt: &'static str,
goal: &'static str,
}
const DEFAULT_ROUTINES: &[DefaultRoutine] =
&[update_moadim::SPEC, the_1_percent::SPEC, token_trim::SPEC];
fn materialize(spec: &DefaultRoutine, now: u64) -> Routine {
Routine {
id: Uuid::new_v4().to_string(),
schedule: normalize_schedule(spec.schedule),
title: spec.title.to_string(),
agent: spec.agent.to_string(),
model: None,
prompt: spec.prompt.to_string(),
goal: Some(spec.goal.to_string()),
repositories: Vec::new(),
machines: vec![crate::machine::current_machine()],
enabled: true,
source: "managed".to_string(),
created_at: now,
updated_at: now,
last_manual_trigger_at: None,
last_scheduled_trigger_at: None,
snoozed_until: None,
skip_runs: None,
power_saving: false,
ttl_secs: None,
max_runtime_secs: None,
tags: Vec::new(),
env: std::collections::HashMap::new(),
}
}
fn reconcile(spec: &DefaultRoutine, cur: &Routine, now: u64) -> Option<Routine> {
let schedule = normalize_schedule(spec.schedule);
let up_to_date = cur.schedule == schedule
&& cur.agent == spec.agent
&& cur.prompt == spec.prompt
&& cur.goal.as_deref() == Some(spec.goal)
&& cur.repositories.is_empty()
&& !cur.machines.is_empty();
if up_to_date {
return None;
}
Some(Routine {
id: cur.id.clone(),
schedule,
title: spec.title.to_string(),
agent: spec.agent.to_string(),
model: cur.model.clone(),
prompt: spec.prompt.to_string(),
goal: Some(spec.goal.to_string()),
repositories: Vec::new(),
machines: if cur.machines.is_empty() {
vec![crate::machine::current_machine()]
} else {
cur.machines.clone()
},
enabled: cur.enabled,
source: "managed".to_string(),
created_at: cur.created_at,
updated_at: now,
last_manual_trigger_at: cur.last_manual_trigger_at,
last_scheduled_trigger_at: cur.last_scheduled_trigger_at,
snoozed_until: cur.snoozed_until,
skip_runs: cur.skip_runs,
power_saving: cur.power_saving,
ttl_secs: cur.ttl_secs,
max_runtime_secs: cur.max_runtime_secs,
tags: cur.tags.clone(),
env: cur.env.clone(),
})
}
pub fn ensure_default_routines(store: &RoutineStore) {
let removed = read_removed_defaults();
for spec in DEFAULT_ROUTINES {
let slug = slugify(spec.title);
let existing = store
.lock_recover()
.values()
.find(|routine| slugify(&routine.title) == slug)
.cloned();
let routine = if let Some(cur) = existing {
match reconcile(spec, &cur, now_secs()) {
Some(updated) => updated,
None => continue,
}
} else {
if removed.contains(&slug) {
continue;
}
materialize(spec, now_secs())
};
if let Err(err) = write_routine(&routine) {
log::warn!(
"ensure_default_routines: failed to write {:?}: {err}; skipping",
spec.title
);
continue;
}
store.lock_recover().insert(routine.id.clone(), routine);
}
}
#[must_use]
pub fn is_default_slug(slug: &str) -> bool {
DEFAULT_ROUTINES
.iter()
.any(|spec| slugify(spec.title) == slug)
}
#[derive(Default, Serialize, Deserialize)]
struct RemovedDefaults {
#[serde(default)]
slugs: BTreeSet<String>,
}
fn read_removed_defaults() -> BTreeSet<String> {
let Ok(raw) = std::fs::read_to_string(removed_default_routines_path()) else {
return BTreeSet::new();
};
toml::from_str::<RemovedDefaults>(&raw)
.map(|removed| removed.slugs)
.unwrap_or_default()
}
fn write_removed_defaults(slugs: &BTreeSet<String>) -> std::io::Result<()> {
let path = removed_default_routines_path();
let parent = crate::utils::fs_perms::parent_or_err(&path, "removed-defaults tombstone")?;
std::fs::create_dir_all(parent)?;
let body = RemovedDefaults {
slugs: slugs.clone(),
};
let toml = toml::to_string_pretty(&body).map_err(std::io::Error::other)?;
std::fs::write(path, toml)
}
fn removed_defaults_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub fn record_removed_default(slug: &str) {
let _guard = removed_defaults_lock().lock_recover();
let mut slugs = read_removed_defaults();
if slugs.insert(slug.to_string()) {
if let Err(err) = write_removed_defaults(&slugs) {
log::warn!("record_removed_default: failed to persist tombstone for {slug:?}: {err}");
}
}
}
pub fn clear_removed_default(slug: &str) {
let _guard = removed_defaults_lock().lock_recover();
let mut slugs = read_removed_defaults();
if slugs.remove(slug) {
if let Err(err) = write_removed_defaults(&slugs) {
log::warn!("clear_removed_default: failed to clear tombstone for {slug:?}: {err}");
}
}
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod defaults_tests;
#[cfg(test)]
#[path = "lock_tests.rs"]
mod defaults_lock_tests;