limits: unified 1 KiB cap on send/ask + reminder auto-file on overflow
This commit is contained in:
parent
753409a5ef
commit
0e6bac8388
6 changed files with 180 additions and 42 deletions
|
|
@ -98,6 +98,9 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
let broker = &coord.broker;
|
||||
match req {
|
||||
AgentRequest::Send { to, body } => {
|
||||
if let Err(message) = crate::limits::check_size("send", body) {
|
||||
return AgentResponse::Err { message };
|
||||
}
|
||||
// Handle broadcast sends (recipient = "*")
|
||||
if to == "*" {
|
||||
let errors = coord.broadcast_send(agent, body);
|
||||
|
|
@ -189,6 +192,9 @@ fn handle_ask_operator(
|
|||
multi: bool,
|
||||
ttl_seconds: Option<u64>,
|
||||
) -> AgentResponse {
|
||||
if let Err(message) = crate::limits::check_size("question", question) {
|
||||
return AgentResponse::Err { message };
|
||||
}
|
||||
let deadline_at = ttl_seconds.and_then(|s| {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
|
@ -214,21 +220,6 @@ fn handle_ask_operator(
|
|||
}
|
||||
}
|
||||
|
||||
/// Cap on the inline `message` byte length when no `file_path` is set.
|
||||
/// Reminders land in the agent's inbox and feed the next wake prompt — a
|
||||
/// multi-kilobyte body bloats every subsequent turn's context. Anything
|
||||
/// bigger should be persisted to disk by the caller and pointed at via
|
||||
/// `file_path` (which the scheduler will deliver as a path reference rather
|
||||
/// than the full body).
|
||||
const REMIND_MESSAGE_MAX: usize = 4096;
|
||||
|
||||
/// Upper cap when `file_path` IS set. The body still lands in the
|
||||
/// reminders sqlite row until delivery, so without an upper bound a
|
||||
/// caller could DOS the broker DB with a single multi-megabyte
|
||||
/// reminder. 64 KiB is generous for any reasonable payload + keeps a
|
||||
/// single row small enough that sqlite won't choke.
|
||||
const REMIND_MESSAGE_MAX_WITH_FILE: usize = 64 * 1024;
|
||||
|
||||
fn handle_remind(
|
||||
coord: &Arc<Coordinator>,
|
||||
agent: &str,
|
||||
|
|
@ -236,21 +227,6 @@ fn handle_remind(
|
|||
timing: &hive_sh4re::ReminderTiming,
|
||||
file_path: Option<&str>,
|
||||
) -> AgentResponse {
|
||||
let (cap, hint) = match file_path {
|
||||
None => (
|
||||
REMIND_MESSAGE_MAX,
|
||||
"; set `file_path` to persist a larger payload to a file instead",
|
||||
),
|
||||
Some(_) => (REMIND_MESSAGE_MAX_WITH_FILE, ""),
|
||||
};
|
||||
if message.len() > cap {
|
||||
return AgentResponse::Err {
|
||||
message: format!(
|
||||
"reminder body too long ({} bytes, max {cap}){hint}",
|
||||
message.len()
|
||||
),
|
||||
};
|
||||
}
|
||||
let due_at = match resolve_due_at(timing) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
|
|
@ -259,7 +235,14 @@ fn handle_remind(
|
|||
};
|
||||
}
|
||||
};
|
||||
match coord.broker.store_reminder(agent, message, file_path, due_at) {
|
||||
let (stored_message, stored_path) = match prepare_remind_storage(agent, message, file_path) {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return AgentResponse::Err { message: e },
|
||||
};
|
||||
match coord
|
||||
.broker
|
||||
.store_reminder(agent, &stored_message, stored_path.as_deref(), due_at)
|
||||
{
|
||||
Ok(id) => {
|
||||
tracing::info!(%id, %agent, %due_at, "reminder scheduled");
|
||||
AgentResponse::Ok
|
||||
|
|
@ -270,6 +253,61 @@ fn handle_remind(
|
|||
}
|
||||
}
|
||||
|
||||
/// Decide what we actually store in the reminders row, applying the
|
||||
/// same byte cap as the rest of the wire protocol
|
||||
/// ([`crate::limits::MESSAGE_MAX_BYTES`]). Three outcomes:
|
||||
///
|
||||
/// 1. Body within the cap → stored verbatim, with whatever `file_path`
|
||||
/// the caller passed (None or Some). The scheduler honours
|
||||
/// `file_path` at delivery time as before.
|
||||
/// 2. Body over the cap, no caller `file_path` → auto-generate a path
|
||||
/// under `/agents/<agent>/state/reminders/auto-<ts>.md`, write the
|
||||
/// body to disk now, store a short pointer hint as the message and
|
||||
/// clear `file_path` (so the scheduler doesn't re-write at
|
||||
/// delivery and overwrite the body with the hint).
|
||||
/// 3. Body over the cap, caller provided `file_path` → honour the
|
||||
/// caller's path: write the body to it now, store the same hint
|
||||
/// and clear `file_path` for the same reason as (2).
|
||||
///
|
||||
/// Returns `(stored_message, stored_file_path)` on success, or a
|
||||
/// caller-ready error string on auto-save failure (which is the only
|
||||
/// way a Remind request can be refused for size — the agent never has
|
||||
/// to think about the cap).
|
||||
fn prepare_remind_storage(
|
||||
agent: &str,
|
||||
message: &str,
|
||||
file_path: Option<&str>,
|
||||
) -> Result<(String, Option<String>), String> {
|
||||
if message.len() <= crate::limits::MESSAGE_MAX_BYTES {
|
||||
return Ok((message.to_owned(), file_path.map(str::to_owned)));
|
||||
}
|
||||
let req_path = match file_path {
|
||||
Some(p) => p.to_owned(),
|
||||
None => auto_reminder_path(agent),
|
||||
};
|
||||
let host_path = crate::reminder_scheduler::resolve_host_path(agent, &req_path)
|
||||
.map_err(|reason| format!("auto-save path `{req_path}` rejected: {reason}"))?;
|
||||
crate::reminder_scheduler::write_payload(agent, &host_path, message)
|
||||
.map_err(|reason| format!("auto-save of large reminder body to `{req_path}` failed: {reason}"))?;
|
||||
let hint = format!(
|
||||
"[reminder body of {} bytes auto-saved to `{req_path}`; read with your filesystem tools]",
|
||||
message.len()
|
||||
);
|
||||
Ok((hint, None))
|
||||
}
|
||||
|
||||
/// Generate a per-agent path for an auto-saved reminder body. Uses
|
||||
/// `unix_nanos` plus the agent name to keep collisions infinitesimal
|
||||
/// across the agent's own state subtree (we're not stamping a hostname
|
||||
/// since hive-c0re is single-host).
|
||||
fn auto_reminder_path(agent: &str) -> String {
|
||||
let ts_ns = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0);
|
||||
format!("/agents/{agent}/state/reminders/auto-{ts_ns}.md")
|
||||
}
|
||||
|
||||
/// Resolve the `due_at` unix timestamp for a Remind request. Returns
|
||||
/// distinct error messages for each failure mode (overflow on
|
||||
/// `InSeconds`, pre-epoch clock, `i64` cast wrap) so the caller can tell
|
||||
|
|
@ -293,3 +331,30 @@ fn resolve_due_at(timing: &hive_sh4re::ReminderTiming) -> anyhow::Result<i64> {
|
|||
ReminderTiming::At { unix_timestamp } => Ok(*unix_timestamp),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn auto_reminder_path_format() {
|
||||
let p = auto_reminder_path("damocles");
|
||||
assert!(p.starts_with("/agents/damocles/state/reminders/auto-"));
|
||||
assert!(p.ends_with(".md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_remind_storage_passthrough_under_cap() {
|
||||
let (msg, fp) = prepare_remind_storage("foo", "small body", None).unwrap();
|
||||
assert_eq!(msg, "small body");
|
||||
assert_eq!(fp, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepare_remind_storage_passthrough_with_caller_file_path() {
|
||||
let (msg, fp) =
|
||||
prepare_remind_storage("foo", "small", Some("/agents/foo/state/x.md")).unwrap();
|
||||
assert_eq!(msg, "small");
|
||||
assert_eq!(fp.as_deref(), Some("/agents/foo/state/x.md"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue