configurable model (default sonnet 4.6), timestamps in chat log

This commit is contained in:
Damocles 2026-04-30 21:02:44 +02:00
parent 84bb5165ef
commit 5ab592071e

View file

@ -29,8 +29,11 @@ struct Config {
username: String, username: String,
password: String, password: String,
rate_limit_per_min: Option<u32>, rate_limit_per_min: Option<u32>,
model: Option<String>,
} }
const DEFAULT_MODEL: &str = "claude-sonnet-4-6";
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
struct PersistedSession { struct PersistedSession {
homeserver: String, homeserver: String,
@ -45,6 +48,8 @@ struct ChatMessage {
sender: OwnedUserId, sender: OwnedUserId,
body: String, body: String,
is_self: bool, is_self: bool,
/// Unix seconds. 0 if unknown.
ts: i64,
} }
struct DaemonState { struct DaemonState {
@ -56,6 +61,7 @@ struct DaemonState {
rate_budget: u32, rate_budget: u32,
rate_limit_per_min: u32, rate_limit_per_min: u32,
last_rate_reset: std::time::Instant, last_rate_reset: std::time::Instant,
model: String,
} }
const MAX_HISTORY: usize = 20; const MAX_HISTORY: usize = 20;
@ -82,6 +88,10 @@ async fn main() -> anyhow::Result<()> {
let rate_limit_per_min = config let rate_limit_per_min = config
.rate_limit_per_min .rate_limit_per_min
.unwrap_or(DEFAULT_RATE_LIMIT_PER_MIN); .unwrap_or(DEFAULT_RATE_LIMIT_PER_MIN);
let model = config
.model
.clone()
.unwrap_or_else(|| DEFAULT_MODEL.to_owned());
let (client, sync_token) = if session_file.exists() { let (client, sync_token) = if session_file.exists() {
restore_session(&session_file).await? restore_session(&session_file).await?
@ -90,7 +100,12 @@ async fn main() -> anyhow::Result<()> {
}; };
let own_user_id = client.user_id().context("not logged in")?.to_owned(); let own_user_id = client.user_id().context("not logged in")?.to_owned();
tracing::info!(user = %own_user_id, rate_limit = rate_limit_per_min, "ready"); tracing::info!(
user = %own_user_id,
rate_limit = rate_limit_per_min,
model = %model,
"ready"
);
// Enable persistent event cache (matrix-sdk's sqlite store keeps the timeline) // Enable persistent event cache (matrix-sdk's sqlite store keeps the timeline)
client client
@ -105,6 +120,7 @@ async fn main() -> anyhow::Result<()> {
rate_budget: rate_limit_per_min, rate_budget: rate_limit_per_min,
rate_limit_per_min, rate_limit_per_min,
last_rate_reset: std::time::Instant::now(), last_rate_reset: std::time::Instant::now(),
model,
})); }));
let processor_state = state.clone(); let processor_state = state.clone();
@ -368,6 +384,19 @@ fn chrono_now() -> String {
format!("{y:04}-{m:02}-{d:02}") format!("{y:04}-{m:02}-{d:02}")
} }
/// Format a unix-seconds timestamp as `YYYY-MM-DD HH:MM` UTC. Returns "?" for 0.
fn format_ts(secs: i64) -> String {
if secs == 0 {
return "?".into();
}
let days = secs.div_euclid(86400);
let day_secs = secs.rem_euclid(86400);
let (y, m, d) = days_to_ymd(days);
let h = day_secs / 3600;
let min = (day_secs % 3600) / 60;
format!("{y:04}-{m:02}-{d:02} {h:02}:{min:02}")
}
/// Convert days-since-1970-01-01 to (year, month, day). Civil-date algorithm. /// Convert days-since-1970-01-01 to (year, month, day). Civil-date algorithm.
fn days_to_ymd(z: i64) -> (i64, u32, u32) { fn days_to_ymd(z: i64) -> (i64, u32, u32) {
let z = z + 719_468; let z = z + 719_468;
@ -429,29 +458,30 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
} }
}; };
let own_user = { let (own_user, model) = {
let state = state.lock().await; let state = state.lock().await;
state.own_user_id.clone() (state.own_user_id.clone(), state.model.clone())
}; };
let chat_msgs: Vec<ChatMessage> = history let chat_msgs: Vec<ChatMessage> = history
.iter() .iter()
.map(|(_, sender, body)| ChatMessage { .map(|(_, sender, body, ts)| ChatMessage {
sender: sender.clone(), sender: sender.clone(),
body: body.clone(), body: body.clone(),
is_self: sender == &own_user, is_self: sender == &own_user,
ts: *ts,
}) })
.collect(); .collect();
// Determine seen split: everything before (and including) prev_last_shown is "seen" // Determine seen split: everything before (and including) prev_last_shown is "seen"
let seen_idx = prev_last_shown let seen_idx = prev_last_shown
.as_ref() .as_ref()
.and_then(|id| history.iter().position(|(eid, _, _)| eid == id)) .and_then(|id| history.iter().position(|(eid, _, _, _)| eid == id))
.map_or(0, |pos| pos + 1); .map_or(0, |pos| pos + 1);
let new_last_event_id = history.last().map(|(eid, _, _)| eid.clone()); let new_last_event_id = history.last().map(|(eid, _, _, _)| eid.clone());
match invoke_claude(&room_id, &room_name, &chat_msgs, seen_idx).await { match invoke_claude(&room_id, &room_name, &chat_msgs, seen_idx, &model).await {
Ok(Some(response)) => { Ok(Some(response)) => {
if let Some(target_room) = client.get_room(&response.room) { if let Some(target_room) = client.get_room(&response.room) {
let content = RoomMessageEventContent::text_plain(&response.body); let content = RoomMessageEventContent::text_plain(&response.body);
@ -489,17 +519,17 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
} }
/// Load the last N text messages from the room's persistent event cache. /// Load the last N text messages from the room's persistent event cache.
/// Returns oldest-first list of (event_id, sender, body). /// Returns oldest-first list of (event_id, sender, body, ts_secs).
async fn load_recent_messages( async fn load_recent_messages(
room: &Room, room: &Room,
limit: usize, limit: usize,
) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String)>> { ) -> anyhow::Result<Vec<(OwnedEventId, OwnedUserId, String, i64)>> {
use matrix_sdk::ruma::events::AnySyncTimelineEvent; use matrix_sdk::ruma::events::AnySyncTimelineEvent;
let (cache, _handles) = room.event_cache().await?; let (cache, _handles) = room.event_cache().await?;
let events = cache.events().await; let events = cache.events().await;
let mut out: Vec<(OwnedEventId, OwnedUserId, String)> = Vec::new(); let mut out: Vec<(OwnedEventId, OwnedUserId, String, i64)> = Vec::new();
for ev in events.iter().rev() { for ev in events.iter().rev() {
if out.len() >= limit { if out.len() >= limit {
break; break;
@ -514,10 +544,13 @@ async fn load_recent_messages(
) = msg ) = msg
{ {
if let MessageType::Text(text) = &orig.content.msgtype { if let MessageType::Text(text) = &orig.content.msgtype {
let ts_ms: u64 = orig.origin_server_ts.0.into();
let ts_secs: i64 = i64::try_from(ts_ms).unwrap_or(0) / 1000;
out.push(( out.push((
orig.event_id.clone(), orig.event_id.clone(),
orig.sender.clone(), orig.sender.clone(),
text.body.clone(), text.body.clone(),
ts_secs,
)); ));
} }
} }
@ -538,6 +571,7 @@ async fn invoke_claude(
room_name: &str, room_name: &str,
history: &[ChatMessage], history: &[ChatMessage],
seen_idx: usize, seen_idx: usize,
model: &str,
) -> 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();
@ -577,7 +611,8 @@ async fn invoke_claude(
writeln!(prompt, "\n[previously seen messages — for context]").unwrap(); writeln!(prompt, "\n[previously seen messages — for context]").unwrap();
for msg in old { 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(); let ts = format_ts(msg.ts);
writeln!(prompt, "[{ts}] {prefix}{}: {}", msg.sender, msg.body).unwrap();
} }
} }
@ -587,7 +622,8 @@ async fn invoke_claude(
} else { } else {
for msg in new { for msg in new {
let prefix = if msg.is_self { "(you) " } else { "" }; let prefix = if msg.is_self { "(you) " } else { "" };
writeln!(prompt, "{prefix}{}: {}", msg.sender, msg.body).unwrap(); let ts = format_ts(msg.ts);
writeln!(prompt, "[{ts}] {prefix}{}: {}", msg.sender, msg.body).unwrap();
} }
} }
@ -598,6 +634,8 @@ async fn invoke_claude(
let mut cmd = Command::new("claude"); let mut cmd = Command::new("claude");
cmd.args([ cmd.args([
"--print", "--print",
"--model",
model,
"--add-dir", "--add-dir",
&identity_str, &identity_str,
"--allowedTools", "--allowedTools",