From f3bb0a6f4b28f9cfefc8fca653a29f2135879c68 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 29 Apr 2026 00:24:47 +0000 Subject: [PATCH] huskies: merge 828 --- server/src/llm/chat/run.rs | 99 +++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/server/src/llm/chat/run.rs b/server/src/llm/chat/run.rs index 7b69e9d1..8c8dc2c6 100644 --- a/server/src/llm/chat/run.rs +++ b/server/src/llm/chat/run.rs @@ -7,6 +7,7 @@ use crate::llm::types::{Message, Role, ToolDefinition}; use crate::slog; use crate::state::SessionState; use crate::store::StoreOps; +use chrono::Utc; use serde::Deserialize; const MAX_TURNS: usize = 30; @@ -32,6 +33,17 @@ pub struct ChatResult { pub session_id: Option, } +/// Prepend an ISO-8601 UTC timestamp to the content of the last user message. +/// +/// Only the most-recent user message is annotated so that prior turns in the +/// conversation history are not re-stamped on every call. Assistant, system, +/// and tool messages are left untouched. +fn inject_received_at(messages: &mut [Message], ts: &str) { + if let Some(msg) = messages.iter_mut().rev().find(|m| m.role == Role::User) { + msg.content = format!("[{ts}] {}", msg.content); + } +} + fn get_anthropic_api_key_impl(store: &dyn StoreOps) -> Result { match store.get(KEY_ANTHROPIC_API_KEY) { Some(value) => { @@ -103,7 +115,7 @@ pub fn cancel_chat(state: &SessionState) -> Result<(), String> { /// Run a multi-turn chat with tool calling against the configured provider. #[allow(clippy::too_many_arguments)] pub async fn chat( - messages: Vec, + mut messages: Vec, config: ProviderConfig, state: &SessionState, store: &dyn StoreOps, @@ -121,6 +133,12 @@ where use crate::llm::providers::anthropic::AnthropicProvider; use crate::llm::providers::ollama::OllamaProvider; + // Stamp the current user message with the wall-clock time at the transport + // boundary (i.e. when this function is entered, which is immediately after + // the WebSocket frame is received — before any LLM invocation). + let received_at = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(); + inject_received_at(&mut messages, &received_at); + let _ = state.cancel_tx.send(false); let mut cancel_rx = state.cancel_rx.clone(); cancel_rx.borrow_and_update(); @@ -474,6 +492,85 @@ mod tests { } } + // --------------------------------------------------------------------------- + // inject_received_at + // --------------------------------------------------------------------------- + + #[test] + fn inject_timestamp_into_last_user_message() { + let mut messages = vec![Message { + role: Role::User, + content: "hello".to_string(), + tool_calls: None, + tool_call_id: None, + }]; + inject_received_at(&mut messages, "2026-04-28T10:30:00Z"); + assert_eq!(messages[0].content, "[2026-04-28T10:30:00Z] hello"); + } + + #[test] + fn inject_timestamp_only_last_user_message() { + let mut messages = vec![ + Message { + role: Role::User, + content: "first".to_string(), + tool_calls: None, + tool_call_id: None, + }, + Message { + role: Role::Assistant, + content: "reply".to_string(), + tool_calls: None, + tool_call_id: None, + }, + Message { + role: Role::User, + content: "second".to_string(), + tool_calls: None, + tool_call_id: None, + }, + ]; + inject_received_at(&mut messages, "2026-04-28T10:30:00Z"); + // Only the last user message is stamped. + assert_eq!(messages[0].content, "first"); + assert_eq!(messages[1].content, "reply"); + assert_eq!(messages[2].content, "[2026-04-28T10:30:00Z] second"); + } + + #[test] + fn inject_timestamp_skips_assistant_messages() { + let mut messages = vec![Message { + role: Role::Assistant, + content: "bot reply".to_string(), + tool_calls: None, + tool_call_id: None, + }]; + inject_received_at(&mut messages, "2026-04-28T10:30:00Z"); + // No user message — nothing changes. + assert_eq!(messages[0].content, "bot reply"); + } + + #[test] + fn inject_timestamp_does_not_stamp_system_messages() { + let mut messages = vec![ + Message { + role: Role::System, + content: "sys".to_string(), + tool_calls: None, + tool_call_id: None, + }, + Message { + role: Role::User, + content: "hello".to_string(), + tool_calls: None, + tool_call_id: None, + }, + ]; + inject_received_at(&mut messages, "2026-04-28T10:30:00Z"); + assert_eq!(messages[0].content, "sys"); + assert_eq!(messages[1].content, "[2026-04-28T10:30:00Z] hello"); + } + // --------------------------------------------------------------------------- // cancel_chat // ---------------------------------------------------------------------------