feat(424): rate-limit traffic-light dots and hard-block alerts
- Add HardBlock variant to WatcherEvent (story_id, agent_name, reset_time) - In pty.rs, distinguish allowed_warning (throttle) from hard blocks; emit RateLimitWarning for throttles, HardBlock for actual 429s - Add `throttled: bool` field to StoryAgent / AgentInfo - Pool spawns a background listener that sets throttled=true on RateLimitWarning or HardBlock events and fires AgentStateChanged - Status command shows traffic-light dots: ○ idle, ● running, ◑ throttled, ✗ blocked - Read blocked flag from story front matter for the ✗ dot - Notifications: RateLimitWarning silenced (too noisy); HardBlock sends urgent chat notification with optional reset time - Tests added for traffic_light_dot, read_story_blocked, status output, and all notification paths
This commit is contained in:
@@ -188,6 +188,8 @@ pub struct AgentInfo {
|
||||
pub completion: Option<CompletionReport>,
|
||||
/// UUID identifying the persistent log file for this session.
|
||||
pub log_session_id: Option<String>,
|
||||
/// True when a rate-limit throttle warning was received for this agent.
|
||||
pub throttled: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -271,6 +271,7 @@ impl AgentPool {
|
||||
project_root: Some(project_root.to_path_buf()),
|
||||
log_session_id: Some(log_session_id.clone()),
|
||||
merge_failure_reported: false,
|
||||
throttled: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,13 +41,47 @@ pub struct AgentPool {
|
||||
|
||||
impl AgentPool {
|
||||
pub fn new(port: u16, watcher_tx: broadcast::Sender<WatcherEvent>) -> Self {
|
||||
Self {
|
||||
let pool = Self {
|
||||
agents: Arc::new(Mutex::new(HashMap::new())),
|
||||
port,
|
||||
child_killers: Arc::new(Mutex::new(HashMap::new())),
|
||||
watcher_tx,
|
||||
watcher_tx: watcher_tx.clone(),
|
||||
merge_jobs: Arc::new(Mutex::new(HashMap::new())),
|
||||
};
|
||||
|
||||
// Spawn a background task (only when inside a tokio runtime) that
|
||||
// listens for RateLimitWarning and HardBlock events and updates the
|
||||
// throttled flag on the relevant agent so status dots stay current.
|
||||
if tokio::runtime::Handle::try_current().is_ok() {
|
||||
let agents_clone = Arc::clone(&pool.agents);
|
||||
let watcher_tx_clone = watcher_tx.clone();
|
||||
let mut rx = watcher_tx.subscribe();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let event = match rx.recv().await {
|
||||
Ok(e) => e,
|
||||
Err(broadcast::error::RecvError::Closed) => break,
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
};
|
||||
let (story_id, agent_name) = match &event {
|
||||
WatcherEvent::RateLimitWarning { story_id, agent_name }
|
||||
| WatcherEvent::HardBlock { story_id, agent_name, .. } => {
|
||||
(story_id.clone(), agent_name.clone())
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
let key = composite_key(&story_id, &agent_name);
|
||||
if let Ok(mut agents) = agents_clone.lock() {
|
||||
if let Some(agent) = agents.get_mut(&key) {
|
||||
agent.throttled = true;
|
||||
}
|
||||
}
|
||||
let _ = watcher_tx_clone.send(WatcherEvent::AgentStateChanged);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
|
||||
@@ -80,6 +80,8 @@ pub(super) struct StoryAgent {
|
||||
/// worktree (which compiles fine) and returns `gates_passed=true` even
|
||||
/// though the code was never squash-merged onto master.
|
||||
pub(super) merge_failure_reported: bool,
|
||||
/// Set to `true` when a rate-limit throttle warning was received for this agent.
|
||||
pub(super) throttled: bool,
|
||||
}
|
||||
|
||||
/// Build an `AgentInfo` snapshot from a `StoryAgent` map entry.
|
||||
@@ -99,5 +101,6 @@ pub(super) fn agent_info_from_entry(story_id: &str, agent: &StoryAgent) -> Agent
|
||||
.map(|wt| wt.base_branch.clone()),
|
||||
completion: agent.completion.clone(),
|
||||
log_session_id: agent.log_session_id.clone(),
|
||||
throttled: agent.throttled,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user