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