Skip to main content

agentmux_srv/ambient/
limits.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! How many ambient CLI calls may run at once. `AmbientGateway` coalesces and
5//! cancels only within one `(entity, purpose)` key; without a cap, N panes
6//! finishing a turn at the same moment would each spawn their own CLI.
7//!
8//! Two classes, each with one cap, so the total is bounded and a burst of
9//! background work can never hold up a call the user is waiting on. These
10//! replace six separate semaphores (pull calls, definition previews, backlog
11//! naming, narration, the recovery sweep's own, and none at all for the
12//! continuity summary), each justified alone, none bounding the total.
13//! docs/reports/REPORT_AMBIENT_FRAMEWORK_REASSESSMENT_2026_10_08.md section 6.4.
14
15use std::time::Duration;
16
17use tokio::sync::Semaphore;
18
19/// Which queue an ambient call waits in.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Class {
22    /// Shown to the user as a result of what they just did: the pane's title, the
23    /// next-message suggestion, a name for a row they opened or a subagent that
24    /// just started.
25    Interactive,
26    /// Fill-in nobody is waiting on: title recovery, previews, backlog naming,
27    /// narration, the continuity summary.
28    Background,
29}
30
31/// Simultaneous interactive calls, across all panes.
32pub const INTERACTIVE_CAP: usize = 2;
33/// Simultaneous background calls, across all panes. One more than interactive:
34/// a continuity summary can run for up to 90 s, and narration should not wait
35/// behind two of them.
36pub const BACKGROUND_CAP: usize = 3;
37
38/// The longest an interactive call waits for a permit before giving up. With
39/// the 30 s interactive time limit (`purpose`), a call ends within 40 s, inside
40/// the pane's 45 s RPC timeout (`ambient-rpc.ts`), so a reply is never paid for
41/// after the pane has stopped waiting for it.
42pub const INTERACTIVE_MAX_QUEUE_WAIT: Duration = Duration::from_secs(10);
43
44impl Class {
45    /// How long a call of this class may wait for a permit; `None` for no limit
46    /// (nobody is waiting on background work).
47    pub fn max_queue_wait(self) -> Option<Duration> {
48        match self {
49            Class::Interactive => Some(INTERACTIVE_MAX_QUEUE_WAIT),
50            Class::Background => None,
51        }
52    }
53
54    /// The class's process-wide semaphore.
55    pub fn semaphore(self) -> &'static Semaphore {
56        static INTERACTIVE: std::sync::OnceLock<Semaphore> = std::sync::OnceLock::new();
57        static BACKGROUND: std::sync::OnceLock<Semaphore> = std::sync::OnceLock::new();
58        match self {
59            Class::Interactive => INTERACTIVE.get_or_init(|| Semaphore::new(INTERACTIVE_CAP)),
60            Class::Background => BACKGROUND.get_or_init(|| Semaphore::new(BACKGROUND_CAP)),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn each_class_is_capped_and_the_two_are_separate() {
71        let interactive = Class::Interactive.semaphore();
72        let background = Class::Background.semaphore();
73        assert!(!std::ptr::eq(interactive, background), "background work must not queue behind interactive work");
74        // Capacity, read without taking permits other tests might be holding.
75        assert!(interactive.available_permits() <= INTERACTIVE_CAP);
76        assert!(background.available_permits() <= BACKGROUND_CAP);
77        assert!(std::ptr::eq(interactive, Class::Interactive.semaphore()), "one semaphore per class, process-wide");
78    }
79}