Skip to main content

ralph/execution_history/
storage.rs

1//! Execution-history persistence helpers.
2//!
3//! Responsibilities:
4//! - Load and save execution-history cache files.
5//! - Append completed executions and prune old entries.
6//!
7//! Not handled here:
8//! - Weighted-average calculations.
9//! - ETA presentation.
10//!
11//! Invariants/assumptions:
12//! - History persists at `.ralph/cache/execution_history.json`.
13//! - Pruning keeps only the newest bounded set of entries.
14
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18
19use anyhow::{Context, Result};
20
21use crate::progress::ExecutionPhase;
22
23use super::model::{ExecutionEntry, ExecutionHistory};
24
25const EXECUTION_HISTORY_FILE: &str = "execution_history.json";
26const MAX_ENTRIES: usize = 100;
27
28fn execution_history_path(cache_dir: &Path) -> PathBuf {
29    cache_dir.join(EXECUTION_HISTORY_FILE)
30}
31
32/// Load execution history from cache directory.
33pub fn load_execution_history(cache_dir: &Path) -> Result<ExecutionHistory> {
34    let path = execution_history_path(cache_dir);
35    if !path.exists() {
36        return Ok(ExecutionHistory::default());
37    }
38
39    let content = fs::read_to_string(&path)
40        .with_context(|| format!("Failed to read execution history from {}", path.display()))?;
41    serde_json::from_str(&content)
42        .with_context(|| format!("Failed to parse execution history from {}", path.display()))
43}
44
45/// Save execution history to cache directory.
46pub fn save_execution_history(history: &ExecutionHistory, cache_dir: &Path) -> Result<()> {
47    fs::create_dir_all(cache_dir)
48        .with_context(|| format!("Failed to create cache directory {}", cache_dir.display()))?;
49
50    let path = execution_history_path(cache_dir);
51    let content =
52        serde_json::to_string_pretty(history).context("Failed to serialize execution history")?;
53    crate::fsutil::write_atomic(&path, content.as_bytes())
54        .with_context(|| format!("Failed to persist execution history to {}", path.display()))
55}
56
57/// Record a completed execution to history.
58pub fn record_execution(
59    task_id: &str,
60    runner: &str,
61    model: &str,
62    phase_count: u8,
63    phase_durations: std::collections::HashMap<ExecutionPhase, Duration>,
64    total_duration: Duration,
65    cache_dir: &Path,
66) -> Result<()> {
67    let mut history = load_execution_history(cache_dir)?;
68    history.entries.push(ExecutionEntry {
69        timestamp: crate::timeutil::now_utc_rfc3339_or_fallback(),
70        task_id: task_id.to_string(),
71        runner: runner.to_string(),
72        model: model.to_string(),
73        phase_count,
74        phase_durations,
75        total_duration,
76    });
77
78    prune_old_entries(&mut history);
79    save_execution_history(&history, cache_dir)
80}
81
82/// Prune oldest entries to keep history bounded.
83pub(crate) fn prune_old_entries(history: &mut ExecutionHistory) {
84    if history.entries.len() <= MAX_ENTRIES {
85        return;
86    }
87
88    history
89        .entries
90        .sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
91    history.entries.truncate(MAX_ENTRIES);
92}