use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use rusqlite::{params, Connection};
const RAW_MARKER: &str = "stopfailure:rate_limit";
const DEFAULT_RUNTIME: &str = "claude-code";
pub fn run(root: &Path, agent: &str) -> Result<()> {
let compose = super::load(root)?;
let db_path = compose.root.join(&compose.global.broker.path);
let runtime = compose
.agents()
.find(|h| h.id() == agent)
.map(|h| h.spec.runtime.clone())
.unwrap_or_else(|| DEFAULT_RUNTIME.to_string());
if let Some(parent) = db_path.parent() {
std::fs::create_dir_all(parent).ok();
}
let conn = Connection::open(&db_path)?;
conn.busy_timeout(Duration::from_secs(5))?;
conn.pragma_update(None, "journal_mode", "WAL")?;
team_core::mailbox::ensure(&conn)?;
record_hit(&conn, agent, &runtime, now())?;
tracing::debug!(agent, "recorded rate-limit hit");
Ok(())
}
fn record_hit(conn: &Connection, agent: &str, runtime: &str, hit_at: f64) -> Result<()> {
conn.execute(
"INSERT INTO rate_limits (agent_id, runtime, hit_at, resets_at, raw_match)
VALUES (?1,?2,?3,?4,?5)",
params![agent, runtime, hit_at, None::<f64>, RAW_MARKER],
)?;
Ok(())
}
fn now() -> f64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
fn db() -> Connection {
let conn = Connection::open_in_memory().expect("open in-memory db");
team_core::mailbox::ensure(&conn).expect("bootstrap schema");
conn
}
#[test]
fn record_hit_writes_a_forensic_marker_row_with_null_resets_at() {
let conn = db();
record_hit(&conn, "alpha:dev", "claude-code", 123.0).expect("record hit");
let (agent_id, runtime, hit_at, resets_at, raw_match): (
String,
String,
f64,
Option<f64>,
String,
) = conn
.query_row(
"SELECT agent_id, runtime, hit_at, resets_at, raw_match FROM rate_limits",
[],
|row| {
Ok((
row.get("agent_id")?,
row.get("runtime")?,
row.get("hit_at")?,
row.get("resets_at")?,
row.get("raw_match")?,
))
},
)
.expect("query the inserted row");
assert_eq!(agent_id, "alpha:dev");
assert_eq!(runtime, "claude-code");
assert_eq!(hit_at, 123.0);
assert_eq!(raw_match, "stopfailure:rate_limit");
assert_eq!(raw_match, RAW_MARKER);
assert_eq!(
resets_at, None,
"resets_at must be NULL for a hook-sourced hit"
);
}
#[test]
fn record_hit_does_not_dedup_repeated_hits() {
let conn = db();
record_hit(&conn, "alpha:dev", "claude-code", 100.0).expect("first hit");
record_hit(&conn, "alpha:dev", "claude-code", 200.0).expect("second hit");
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM rate_limits", [], |row| row.get(0))
.expect("count rows");
assert_eq!(count, 2, "two hits must produce two rows, no dedup");
}
}