diff --git a/src/main.rs b/src/main.rs index f9699c3..96aeca9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,6 +49,9 @@ struct ChatMessage { struct DaemonState { own_user_id: OwnedUserId, room_history: std::collections::HashMap>, + /// For each room, the index in room_history up to which messages have been + /// shown to Claude. Messages at indexes >= this value are "new". + seen_count: std::collections::HashMap, pending_rooms: Vec, rate_budget: u32, rate_limit_per_min: u32, @@ -92,6 +95,7 @@ async fn main() -> anyhow::Result<()> { let state = Arc::new(Mutex::new(DaemonState { own_user_id, room_history: std::collections::HashMap::new(), + seen_count: std::collections::HashMap::new(), pending_rooms: Vec::new(), rate_budget: rate_limit_per_min, rate_limit_per_min, @@ -282,20 +286,22 @@ async fn process_loop(state: Arc>, client: Client) { continue; }; - let history = { + let (history, seen_idx) = { let state = state.lock().await; - state + let history = state .room_history .get(&room_id) .cloned() - .unwrap_or_default() + .unwrap_or_default(); + let seen = state.seen_count.get(&room_id).copied().unwrap_or(0); + (history, seen) }; let room_name = client .get_room(&room_id) .map_or_else(|| room_id.to_string(), |r| r.room_id().to_string()); - match invoke_claude(&room_id, &room_name, &history).await { + match invoke_claude(&room_id, &room_name, &history, seen_idx).await { Ok(Some(response)) => { if let Some(room) = client.get_room(&response.room) { let content = RoomMessageEventContent::text_plain(&response.body); @@ -303,6 +309,8 @@ async fn process_loop(state: Arc>, client: Client) { Ok(_) => { let mut state = state.lock().await; state.rate_budget = state.rate_budget.saturating_sub(1); + // Mark current history as seen + state.seen_count.insert(room_id.clone(), history.len()); tracing::info!( room = %response.room, "sent response ({} budget remaining)", @@ -317,6 +325,9 @@ async fn process_loop(state: Arc>, client: Client) { } Ok(None) => { tracing::debug!(room = %room_id, "claude chose to skip"); + // Even on skip, mark messages as seen so we don't reprocess + let mut state = state.lock().await; + state.seen_count.insert(room_id.clone(), history.len()); } Err(e) => { tracing::error!(room = %room_id, "claude invocation failed: {e}"); @@ -334,21 +345,43 @@ async fn invoke_claude( source_room: &OwnedRoomId, room_name: &str, history: &[ChatMessage], + seen_idx: usize, ) -> anyhow::Result> { let identity_dir = paths::identity_dir(); let identity_str = identity_dir.to_string_lossy(); let mut prompt = String::new(); - writeln!(prompt, "[room: {source_room} ({room_name})]").unwrap(); - writeln!(prompt, "[new messages below this line]").unwrap(); + writeln!(prompt, "[room_id: {source_room}]").unwrap(); + writeln!(prompt, "[room_name: {room_name}]").unwrap(); + writeln!( + prompt, + "[room notes path: ../rooms/{source_room}/notes.md (create dir if needed)]" + ) + .unwrap(); - for msg in history { - let prefix = if msg.is_self { "(you) " } else { "" }; - writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap(); + let seen = seen_idx.min(history.len()); + let (old, new) = history.split_at(seen); + + if !old.is_empty() { + writeln!(prompt, "\n[previously seen messages — for context]").unwrap(); + for msg in old { + let prefix = if msg.is_self { "(you) " } else { "" }; + writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap(); + } } - tracing::info!("invoking claude with {} messages", history.len()); - tracing::debug!("prompt: {prompt}"); + writeln!(prompt, "\n[new messages — respond to these]").unwrap(); + if new.is_empty() { + writeln!(prompt, "(none)").unwrap(); + } else { + for msg in new { + let prefix = if msg.is_self { "(you) " } else { "" }; + writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap(); + } + } + + tracing::info!("invoking claude: {} new, {} seen", new.len(), old.len()); + tracing::trace!("full prompt:\n{prompt}"); use tokio::process::Command; let mut cmd = Command::new("claude");