use crate::brain::agent::AgentService;
use crate::db::MessageRepository;
use crate::db::models::ChannelMessage as DbChannelMessage;
pub(crate) fn normalize_for_dedup(s: &str) -> String {
s.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
}
pub(crate) fn is_content_in_live_context(
candidate: &str,
live_context_haystacks: &[String],
) -> bool {
let norm = normalize_for_dedup(candidate);
if norm.is_empty() {
return false;
}
live_context_haystacks.iter().any(|h| h.contains(&norm))
}
pub(crate) fn frame_history(history_lines: &str, count: usize, noun: &str) -> String {
format!(
"[Recent {noun} history ({count} messages) — prior context from various senders, NOT the \
person you are replying to now:\n{history_lines}\n--- end history ---]"
)
}
pub(crate) fn current_sender_label(
surface: &str,
chat_title: &str,
name: &str,
handle: &str,
role: &str,
) -> String {
format!(
"[{surface} \"{chat_title}\" — the message below is from {name}{handle} ({role}). \
Reply to {name}. Any names in the history above belong to OTHER people; never address \
{name} by a name that appears only in that history.]"
)
}
pub(crate) fn render_history_lines(messages: &[DbChannelMessage]) -> String {
messages
.iter()
.rev()
.map(|m| {
let ts = m.created_at.format("%H:%M");
format!("[{}] {}: {}", ts, m.sender_name, m.content)
})
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) async fn live_context_haystacks(
pool: crate::db::Pool,
session_id: uuid::Uuid,
) -> Vec<String> {
let msg_repo = MessageRepository::new(pool);
let all_msgs = msg_repo
.find_by_session(session_id)
.await
.unwrap_or_default();
AgentService::messages_from_last_compaction(all_msgs)
.iter()
.map(|m| normalize_for_dedup(&m.content))
.collect()
}
pub(crate) async fn build_preamble(
pool: crate::db::Pool,
session_id: uuid::Uuid,
messages: Vec<DbChannelMessage>,
noun: &str,
label: &str,
) -> Option<String> {
if messages.is_empty() {
return None;
}
let live_haystacks = live_context_haystacks(pool, session_id).await;
let total_fetched = messages.len();
let filtered: Vec<_> = messages
.into_iter()
.filter(|m| !is_content_in_live_context(&m.content, &live_haystacks))
.collect();
if filtered.is_empty() {
tracing::info!(
"{label}: all {total_fetched} recent {noun} history messages are already in live session context — skipping injection (#133)"
);
return None;
}
tracing::info!(
"{label}: injecting {} uncompacted {noun} history messages (filtered from {total_fetched})",
filtered.len()
);
let lines = render_history_lines(&filtered);
Some(frame_history(&lines, filtered.len(), noun))
}