Skip to main content

origin_mcp_http/
activity.rs

1//! Endpoint activity tracking (G18).
2//!
3//! "An external AI is driving this application" has to be *visible*, not just logged.
4//! The endpoint records when it last served a request; a host reads that to raise a
5//! tray indicator while the AI is connected and clear it once the endpoint has been
6//! quiet for a while.
7
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11/// A cloneable view of when the endpoint last served a request.
12#[derive(Debug, Clone, Default)]
13pub struct Activity {
14    last: Arc<Mutex<Option<Instant>>>,
15}
16
17impl Activity {
18    /// Record that a request just arrived.
19    pub fn touch(&self) {
20        *self.last.lock().expect("activity poisoned") = Some(Instant::now());
21    }
22
23    /// Whether any request has ever arrived.
24    pub fn has_seen_activity(&self) -> bool {
25        self.last.lock().expect("activity poisoned").is_some()
26    }
27
28    /// Whether the endpoint has been quiet for at least `idle`.
29    ///
30    /// An endpoint that has never served a request counts as idle.
31    pub fn is_idle_for(&self, idle: Duration) -> bool {
32        match *self.last.lock().expect("activity poisoned") {
33            None => true,
34            Some(at) => at.elapsed() >= idle,
35        }
36    }
37
38    /// Whether a session looks active: activity has been seen and it is recent.
39    pub fn is_active(&self, idle: Duration) -> bool {
40        self.has_seen_activity() && !self.is_idle_for(idle)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn a_fresh_activity_has_seen_nothing_and_is_idle() {
50        let activity = Activity::default();
51
52        assert!(!activity.has_seen_activity());
53        assert!(activity.is_idle_for(Duration::from_secs(1)));
54        assert!(!activity.is_active(Duration::from_secs(60)));
55    }
56
57    #[test]
58    fn touching_makes_it_active_then_idle_again() {
59        let activity = Activity::default();
60        activity.touch();
61
62        assert!(activity.has_seen_activity());
63        assert!(activity.is_active(Duration::from_secs(60)));
64        assert!(!activity.is_idle_for(Duration::from_secs(60)));
65
66        // Any non-zero idle window has elapsed after a sleep.
67        std::thread::sleep(Duration::from_millis(20));
68        assert!(activity.is_idle_for(Duration::from_millis(10)));
69        assert!(!activity.is_active(Duration::from_millis(10)));
70    }
71}