Skip to main content

orch/cli/
cooldown.rs

1//! CLI handlers for `orch cooldown list` and `orch cooldown clear`.
2//!
3//! Cooldowns are persisted to the SQLite KV store so they survive restarts.
4//! The `list` command opens the store, hydrates the in-memory map, then
5//! prints all active entries — accurate even when the service is not running.
6
7use crate::cli;
8use crate::engine::cooldown;
9use crate::engine::router::Router;
10use std::sync::Arc;
11
12/// List all active agent and model cooldowns.
13pub async fn list() -> anyhow::Result<()> {
14    let store = Arc::new(cli::init_store().await?);
15    // Hydrate in-memory map from persisted KV so we see the full state.
16    cooldown::init_cooldown_store(Arc::clone(&store)).await;
17
18    // Recompute degraded agents so the CLI shows the same skip reasons as the
19    // service, including agents held only by the rate-limit threshold
20    let router = Router::from_config();
21    router.refresh_health(&store).await;
22
23    let entries = cooldown::list_all_cooldowns();
24    let degraded = cooldown::list_degraded_agents();
25    if entries.is_empty() && degraded.is_empty() {
26        println!("No active cooldowns.");
27        return Ok(());
28    }
29
30    let now = chrono::Utc::now().timestamp();
31    println!("{:<30} {:<12} REASON", "KEY", "REMAINING");
32    println!("{}", "-".repeat(72_usize));
33    for (key, until, reason) in &entries {
34        let remaining_secs = until - now;
35        let remaining = format_remaining(remaining_secs);
36        println!("{key:<30} {remaining:<12} {reason}");
37    }
38    for (agent, reason) in degraded {
39        if entries.iter().any(|(key, ..)| key == &agent) {
40            continue;
41        }
42        println!("{:<30} {:<12} {}", agent, "degraded", reason);
43    }
44    Ok(())
45}
46
47/// Clear a specific cooldown by key, or all cooldowns with `--all`.
48pub async fn clear(key: Option<String>, all: bool) -> anyhow::Result<()> {
49    let store = Arc::new(cli::init_store().await?);
50    // Hydrate first so clear operates on the current state.
51    cooldown::init_cooldown_store(Arc::clone(&store)).await;
52
53    if all {
54        let agents = Router::from_config().available_agents;
55        cooldown::clear_cooldown("*", &store).await;
56        cooldown::mark_rate_limits_cleared(&store, &agents).await;
57        println!("Cleared all cooldowns.");
58    } else if let Some(k) = key {
59        cooldown::clear_cooldown(&k, &store).await;
60        println!("Cleared cooldown: {k}");
61    } else {
62        anyhow::bail!("specify a key (e.g. 'claude' or 'claude:sonnet') or use --all");
63    }
64    Ok(())
65}
66
67fn format_remaining(secs: i64) -> String {
68    if secs <= 0 {
69        return "expired".to_string();
70    }
71    if secs < 3600 {
72        format!("{}m", secs / 60)
73    } else if secs < 86400 {
74        let h = secs / 3600;
75        let m = (secs % 3600) / 60;
76        if m == 0 {
77            format!("{h}h")
78        } else {
79            format!("{h}h{m}m")
80        }
81    } else {
82        let d = secs / 86400;
83        let h = (secs % 86400) / 3600;
84        if h == 0 {
85            format!("{d}d")
86        } else {
87            format!("{d}d{h}h")
88        }
89    }
90}