diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 87daea2..2df2a09 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -308,6 +308,20 @@ fn handle_remind( /// dance (see [`prepare_remind_storage`]), and writes the reminder /// row. Returns `Ok(())` on success, or a caller-ready error string /// the dispatcher wraps in `*Response::Err`. +/// Maximum pending (un-delivered) reminders per agent. Exceeding this +/// causes `store_remind` to return an error so the agent knows to back +/// off instead of silently dropping. Override via +/// `HIVE_REMIND_MAX_PENDING_PER_AGENT`; set to `0` to disable the cap +/// (not recommended — a runaway agent can still flood the scheduler). +const DEFAULT_REMIND_MAX_PENDING: u64 = 50; + +fn remind_max_pending() -> u64 { + std::env::var("HIVE_REMIND_MAX_PENDING_PER_AGENT") + .ok() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(DEFAULT_REMIND_MAX_PENDING) +} + pub(crate) fn store_remind( coord: &Arc, agent: &str, @@ -315,6 +329,21 @@ pub(crate) fn store_remind( timing: &hive_sh4re::ReminderTiming, file_path: Option<&str>, ) -> Result<(), String> { + let max = remind_max_pending(); + if max > 0 { + let pending = coord + .broker + .count_pending_reminders_for(agent) + .unwrap_or(0); + if pending >= max { + return Err(format!( + "reminder rejected: agent `{agent}` already has {pending} pending \ + reminders (cap {max}). Cancel some via `cancel_loose_end` or wait \ + for them to fire before scheduling more. Override the cap with \ + `HIVE_REMIND_MAX_PENDING_PER_AGENT`." + )); + } + } let due_at = resolve_due_at(timing).map_err(|e| format!("invalid reminder timing: {e:#}"))?; let (stored_message, stored_path) = prepare_remind_storage(agent, message, file_path)?; let id = coord