configurable rate limit, paths module, verify and bootstrap binaries

This commit is contained in:
Damocles 2026-04-30 18:15:00 +02:00
parent 888eddf093
commit 0a1246d1f8
7 changed files with 310 additions and 33 deletions

View file

@ -27,6 +27,7 @@ struct Config {
homeserver: String,
username: String,
password: String,
rate_limit_per_min: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
@ -50,11 +51,12 @@ struct DaemonState {
room_history: std::collections::HashMap<OwnedRoomId, Vec<ChatMessage>>,
pending_rooms: Vec<OwnedRoomId>,
rate_budget: u32,
rate_limit_per_min: u32,
last_rate_reset: std::time::Instant,
}
const MAX_HISTORY: usize = 20;
const RATE_LIMIT_PER_MIN: u32 = 2;
const DEFAULT_RATE_LIMIT_PER_MIN: u32 = 1;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@ -65,28 +67,34 @@ async fn main() -> anyhow::Result<()> {
tracing::info!("damocles-daemon starting");
let state_dir = paths::state_dir();
let identity_dir = paths::identity_dir();
fs::create_dir_all(&state_dir).await?;
fs::create_dir_all(&identity_dir).await?;
fs::create_dir_all(paths::identity_dir()).await?;
fs::create_dir_all(state_dir.join("rooms")).await?;
fs::create_dir_all(state_dir.join("people")).await?;
let session_file = paths::session_path();
let db_path = paths::db_path();
let config = load_config().await?;
let rate_limit_per_min = config
.rate_limit_per_min
.unwrap_or(DEFAULT_RATE_LIMIT_PER_MIN);
let (client, sync_token) = if session_file.exists() {
restore_session(&session_file).await?
} else {
let config = load_config().await?;
(login(&config, &db_path, &session_file).await?, None)
};
let own_user_id = client.user_id().context("not logged in")?.to_owned();
tracing::info!(user = %own_user_id, "ready");
tracing::info!(user = %own_user_id, rate_limit = rate_limit_per_min, "ready");
let state = Arc::new(Mutex::new(DaemonState {
own_user_id,
room_history: std::collections::HashMap::new(),
pending_rooms: Vec::new(),
rate_budget: RATE_LIMIT_PER_MIN,
rate_budget: rate_limit_per_min,
rate_limit_per_min,
last_rate_reset: std::time::Instant::now(),
}));
@ -114,7 +122,7 @@ async fn restore_session(session_file: &Path) -> anyhow::Result<(Client, Option<
let client = Client::builder()
.homeserver_url(&session.homeserver)
.sqlite_store(&session.db_path, None)
.sqlite_store(paths::db_path(), None)
.build()
.await?;
@ -259,7 +267,7 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
let mut state = state.lock().await;
if state.last_rate_reset.elapsed() >= std::time::Duration::from_secs(60) {
state.rate_budget = RATE_LIMIT_PER_MIN;
state.rate_budget = state.rate_limit_per_min;
state.last_rate_reset = std::time::Instant::now();
}
@ -339,26 +347,38 @@ async fn invoke_claude(
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap();
}
tracing::debug!("invoking claude with {} messages", history.len());
tracing::info!("invoking claude with {} messages", history.len());
tracing::debug!("prompt: {prompt}");
let output = tokio::process::Command::new("claude")
.args([
"--print",
"--bare",
"--add-dir",
&identity_str,
"--allowedTools",
"Read Edit Write Glob Grep",
])
.current_dir(&identity_dir)
.arg(&prompt)
.output()
.await
.context("failed to run claude")?;
use tokio::process::Command;
let mut cmd = Command::new("claude");
cmd.args([
"--print",
"--add-dir",
&identity_str,
"--allowedTools",
"Read Edit Write Glob Grep",
"-p",
&prompt,
]);
cmd.current_dir(&identity_dir);
cmd.stdin(std::process::Stdio::null());
let output = cmd.output().await.context("failed to run claude")?;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("claude exited with {}: {stderr}", output.status);
bail!(
"claude exited with {}:\nstdout: {}\nstderr: {}",
output.status,
stdout,
stderr
);
}
if !stderr.is_empty() {
tracing::warn!("claude stderr: {stderr}");
}
let raw = String::from_utf8_lossy(&output.stdout).to_string();