systemprompt_loader/bundle/
degraded.rs1use std::fs;
12
13use serde::{Deserialize, Serialize};
14
15use super::cache::BundleCache;
16use super::error::{BundleError, BundleResult};
17use systemprompt_manifest::services::root::ServicesProvenance;
18
19const DEGRADED_FILE: &str = "degraded.json";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct DegradedRecord {
23 pub serving: String,
24 pub error: String,
25 pub recorded_at: String,
26}
27
28impl DegradedRecord {
29 #[must_use]
30 pub fn for_provenance(provenance: &ServicesProvenance) -> Option<Self> {
31 let (serving, error) = match provenance {
32 ServicesProvenance::LastGood { error, .. } => ("last_good", error),
33 ServicesProvenance::BundledFallback { error } => ("bundled", error),
34 ServicesProvenance::Bundled | ServicesProvenance::Fetched { .. } => return None,
35 };
36 Some(Self {
37 serving: serving.to_owned(),
38 error: error.clone(),
39 recorded_at: chrono::Utc::now().to_rfc3339(),
40 })
41 }
42}
43
44impl BundleCache {
45 #[must_use]
46 pub fn read_degraded(&self) -> Option<DegradedRecord> {
47 let raw = fs::read_to_string(self.root().join(DEGRADED_FILE)).ok()?;
48 serde_json::from_str(&raw).ok()
49 }
50
51 pub fn write_degraded(&self, record: &DegradedRecord) -> BundleResult<()> {
52 self.prepare()?;
53 let body = serde_json::to_vec_pretty(record)
54 .map_err(|e| BundleError::policy_context("degraded record is not serialisable", e))?;
55 let tmp = self
56 .root()
57 .join(format!("{DEGRADED_FILE}.tmp-{}", std::process::id()));
58 fs::write(&tmp, body)?;
59 fs::rename(&tmp, self.root().join(DEGRADED_FILE))?;
60 Ok(())
61 }
62
63 pub fn clear_degraded(&self) -> BundleResult<()> {
64 match fs::remove_file(self.root().join(DEGRADED_FILE)) {
65 Ok(()) => Ok(()),
66 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
67 Err(error) => Err(error.into()),
68 }
69 }
70}