pass message history with seen/new separator, room notes path, trace logging for full prompt
This commit is contained in:
parent
0a1246d1f8
commit
9aa85549b5
1 changed files with 44 additions and 11 deletions
51
src/main.rs
51
src/main.rs
|
|
@ -49,6 +49,9 @@ struct ChatMessage {
|
||||||
struct DaemonState {
|
struct DaemonState {
|
||||||
own_user_id: OwnedUserId,
|
own_user_id: OwnedUserId,
|
||||||
room_history: std::collections::HashMap<OwnedRoomId, Vec<ChatMessage>>,
|
room_history: std::collections::HashMap<OwnedRoomId, Vec<ChatMessage>>,
|
||||||
|
/// 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<OwnedRoomId, usize>,
|
||||||
pending_rooms: Vec<OwnedRoomId>,
|
pending_rooms: Vec<OwnedRoomId>,
|
||||||
rate_budget: u32,
|
rate_budget: u32,
|
||||||
rate_limit_per_min: u32,
|
rate_limit_per_min: u32,
|
||||||
|
|
@ -92,6 +95,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let state = Arc::new(Mutex::new(DaemonState {
|
let state = Arc::new(Mutex::new(DaemonState {
|
||||||
own_user_id,
|
own_user_id,
|
||||||
room_history: std::collections::HashMap::new(),
|
room_history: std::collections::HashMap::new(),
|
||||||
|
seen_count: std::collections::HashMap::new(),
|
||||||
pending_rooms: Vec::new(),
|
pending_rooms: Vec::new(),
|
||||||
rate_budget: rate_limit_per_min,
|
rate_budget: rate_limit_per_min,
|
||||||
rate_limit_per_min,
|
rate_limit_per_min,
|
||||||
|
|
@ -282,20 +286,22 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let history = {
|
let (history, seen_idx) = {
|
||||||
let state = state.lock().await;
|
let state = state.lock().await;
|
||||||
state
|
let history = state
|
||||||
.room_history
|
.room_history
|
||||||
.get(&room_id)
|
.get(&room_id)
|
||||||
.cloned()
|
.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
|
let room_name = client
|
||||||
.get_room(&room_id)
|
.get_room(&room_id)
|
||||||
.map_or_else(|| room_id.to_string(), |r| r.room_id().to_string());
|
.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)) => {
|
Ok(Some(response)) => {
|
||||||
if let Some(room) = client.get_room(&response.room) {
|
if let Some(room) = client.get_room(&response.room) {
|
||||||
let content = RoomMessageEventContent::text_plain(&response.body);
|
let content = RoomMessageEventContent::text_plain(&response.body);
|
||||||
|
|
@ -303,6 +309,8 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
state.rate_budget = state.rate_budget.saturating_sub(1);
|
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!(
|
tracing::info!(
|
||||||
room = %response.room,
|
room = %response.room,
|
||||||
"sent response ({} budget remaining)",
|
"sent response ({} budget remaining)",
|
||||||
|
|
@ -317,6 +325,9 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::debug!(room = %room_id, "claude chose to skip");
|
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) => {
|
Err(e) => {
|
||||||
tracing::error!(room = %room_id, "claude invocation failed: {e}");
|
tracing::error!(room = %room_id, "claude invocation failed: {e}");
|
||||||
|
|
@ -334,21 +345,43 @@ async fn invoke_claude(
|
||||||
source_room: &OwnedRoomId,
|
source_room: &OwnedRoomId,
|
||||||
room_name: &str,
|
room_name: &str,
|
||||||
history: &[ChatMessage],
|
history: &[ChatMessage],
|
||||||
|
seen_idx: usize,
|
||||||
) -> anyhow::Result<Option<ClaudeResponse>> {
|
) -> anyhow::Result<Option<ClaudeResponse>> {
|
||||||
let identity_dir = paths::identity_dir();
|
let identity_dir = paths::identity_dir();
|
||||||
let identity_str = identity_dir.to_string_lossy();
|
let identity_str = identity_dir.to_string_lossy();
|
||||||
|
|
||||||
let mut prompt = String::new();
|
let mut prompt = String::new();
|
||||||
writeln!(prompt, "[room: {source_room} ({room_name})]").unwrap();
|
writeln!(prompt, "[room_id: {source_room}]").unwrap();
|
||||||
writeln!(prompt, "[new messages below this line]").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 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 { "" };
|
let prefix = if msg.is_self { "(you) " } else { "" };
|
||||||
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap();
|
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tracing::info!("invoking claude with {} messages", history.len());
|
writeln!(prompt, "\n[new messages — respond to these]").unwrap();
|
||||||
tracing::debug!("prompt: {prompt}");
|
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;
|
use tokio::process::Command;
|
||||||
let mut cmd = Command::new("claude");
|
let mut cmd = Command::new("claude");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue