fn reconcile(spec: &DefaultRoutine, cur: &Routine, now: u64) -> Option<Routine> {
let schedule = normalize_schedule(spec.schedule);
let schedules = vec![schedule.clone()];
let up_to_date = cur.schedule == schedule
&& cur.effective_schedules() == schedules
&& 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,
schedules,
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,
power_saving_exempt: cur.power_saving_exempt,
consecutive_failures: cur.consecutive_failures,
auto_disabled_reason: cur.auto_disabled_reason.clone(),
ttl_secs: cur.ttl_secs,
max_runtime_secs: cur.max_runtime_secs,
failure_threshold: cur.failure_threshold,
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()
}
#[cfg(test)]
#[path = "mod_tests.rs"]
mod defaults_tests;
#[cfg(test)]
#[path = "lock_tests.rs"]
mod defaults_lock_tests;