huskies: merge 527_story_remove_rate_limit_hard_block_bot_notifications_from_matrix_chat

This commit is contained in:
dave
2026-04-10 11:23:19 +00:00
parent 7e5b9839e8
commit fe405e81c6
@@ -141,30 +141,6 @@ const RATE_LIMIT_DEBOUNCE: Duration = Duration::from_secs(60);
/// into a single notification (only the final stage is announced). /// into a single notification (only the final stage is announced).
const STAGE_TRANSITION_DEBOUNCE: Duration = Duration::from_millis(200); const STAGE_TRANSITION_DEBOUNCE: Duration = Duration::from_millis(200);
/// Format a rate limit hard block notification message with scheduled resume time.
///
/// Returns `(plain_text, html)` suitable for `ChatTransport::send_message`.
pub fn format_rate_limit_hard_block_notification(
item_id: &str,
story_name: Option<&str>,
agent_name: &str,
resume_at: chrono::DateTime<chrono::Utc>,
) -> (String, String) {
let number = extract_story_number(item_id).unwrap_or(item_id);
let name = story_name.unwrap_or(item_id);
let local_time = resume_at.with_timezone(&chrono::Local);
let resume_str = local_time.format("%Y-%m-%d %H:%M").to_string();
let plain = format!(
"\u{1f6d1} #{number} {name} \u{2014} {agent_name} hit a hard rate limit; \
will auto-resume at {resume_str}"
);
let html = format!(
"\u{1f6d1} <strong>#{number}</strong> <em>{name}</em> \u{2014} \
{agent_name} hit a hard rate limit; will auto-resume at {resume_str}"
);
(plain, html)
}
/// Format a rate limit warning notification message. /// Format a rate limit warning notification message.
/// ///
@@ -383,39 +359,13 @@ pub fn spawn_notification_listener(
ref agent_name, ref agent_name,
reset_at, reset_at,
}) => { }) => {
// Debounce: reuse the same key as RateLimitWarning so both // Log server-side for debugging; do NOT post to Matrix.
// types are rate-limited together for the same agent. // Hard-block auto-resume is normal operation — the status
let debounce_key = format!("{story_id}:{agent_name}"); // command already surfaces rate-limit state via emoji.
let now = Instant::now();
if let Some(&last) = rate_limit_last_notified.get(&debounce_key)
&& now.duration_since(last) < RATE_LIMIT_DEBOUNCE
{
slog!( slog!(
"[bot] Rate-limit hard-block notification debounced for \ "[bot] Rate-limit hard block for {story_id}/{agent_name}, \
{story_id}:{agent_name}" auto-resume at {reset_at}"
); );
continue;
}
rate_limit_last_notified.insert(debounce_key, now);
let story_name = find_story_name_any_stage(&project_root, story_id);
let (plain, html) = format_rate_limit_hard_block_notification(
story_id,
story_name.as_deref(),
agent_name,
reset_at,
);
slog!("[bot] Sending rate-limit hard-block notification: {plain}");
for room_id in &get_room_ids() {
if let Err(e) = transport.send_message(room_id, &plain, &html).await {
slog!(
"[bot] Failed to send rate-limit hard-block notification \
to {room_id}: {e}"
);
}
}
} }
Ok(WatcherEvent::ConfigChanged) => { Ok(WatcherEvent::ConfigChanged) => {
// Hot-reload: pick up any changes to rate_limit_notifications. // Hot-reload: pick up any changes to rate_limit_notifications.
@@ -1062,17 +1012,10 @@ mod tests {
assert_eq!(calls.len(), 0, "RateLimitWarning should be suppressed when rate_limit_notifications = false"); assert_eq!(calls.len(), 0, "RateLimitWarning should be suppressed when rate_limit_notifications = false");
} }
/// AC3: RateLimitHardBlock is always sent regardless of rate_limit_notifications. /// RateLimitHardBlock is never posted to Matrix — it is logged server-side only.
#[tokio::test] #[tokio::test]
async fn rate_limit_hard_block_always_sent_when_config_false() { async fn rate_limit_hard_block_never_sent_to_matrix() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let sk_dir = tmp.path().join(".huskies");
std::fs::create_dir_all(&sk_dir).unwrap();
std::fs::write(
sk_dir.join("project.toml"),
"rate_limit_notifications = false\n",
)
.unwrap();
let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16); let (watcher_tx, watcher_rx) = broadcast::channel::<WatcherEvent>(16);
let (transport, calls) = MockTransport::new(); let (transport, calls) = MockTransport::new();
@@ -1094,7 +1037,7 @@ mod tests {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let calls = calls.lock().unwrap(); let calls = calls.lock().unwrap();
assert_eq!(calls.len(), 1, "RateLimitHardBlock should always be sent"); assert_eq!(calls.len(), 0, "RateLimitHardBlock must not post to Matrix");
} }
/// AC3: StoryBlocked is always sent regardless of rate_limit_notifications. /// AC3: StoryBlocked is always sent regardless of rate_limit_notifications.