fix(channel): normalize telegram history for MiniMax

This commit is contained in:
Chummy 2026-02-20 01:06:07 +08:00
parent 7173045f1c
commit c5834b1077
2 changed files with 135 additions and 9 deletions

View file

@ -200,6 +200,43 @@ fn channel_delivery_instructions(channel_name: &str) -> Option<&'static str> {
}
}
fn build_channel_system_prompt(base_prompt: &str, channel_name: &str) -> String {
if let Some(instructions) = channel_delivery_instructions(channel_name) {
if base_prompt.is_empty() {
instructions.to_string()
} else {
format!("{base_prompt}\n\n{instructions}")
}
} else {
base_prompt.to_string()
}
}
fn normalize_cached_channel_turns(turns: Vec<ChatMessage>) -> Vec<ChatMessage> {
let mut normalized = Vec::with_capacity(turns.len());
let mut expecting_user = true;
for turn in turns {
match (expecting_user, turn.role.as_str()) {
(true, "user") => {
normalized.push(turn);
expecting_user = false;
}
(false, "assistant") => {
normalized.push(turn);
expecting_user = true;
}
_ => {}
}
}
if normalized.last().map_or(false, |msg| msg.role == "user") {
normalized.pop();
}
normalized
}
fn supports_runtime_model_switch(channel_name: &str) -> bool {
matches!(channel_name, "telegram" | "discord")
}
@ -318,7 +355,7 @@ fn compact_sender_history(ctx: &ChannelRuntimeContext, sender_key: &str) -> bool
let keep_from = turns
.len()
.saturating_sub(CHANNEL_HISTORY_COMPACT_KEEP_MESSAGES);
let mut compacted = turns[keep_from..].to_vec();
let mut compacted = normalize_cached_channel_turns(turns[keep_from..].to_vec());
for turn in &mut compacted {
if turn.content.chars().count() > CHANNEL_HISTORY_COMPACT_CONTENT_CHARS {
@ -327,6 +364,11 @@ fn compact_sender_history(ctx: &ChannelRuntimeContext, sender_key: &str) -> bool
}
}
if compacted.is_empty() {
turns.clear();
return false;
}
*turns = compacted;
true
}
@ -783,21 +825,19 @@ async fn process_channel_message(
ChatMessage::user(&enriched_message),
);
let mut prior_turns = ctx
// Build history from per-sender conversation cache.
let prior_turns_raw = ctx
.conversation_histories
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&history_key)
.cloned()
.unwrap_or_default();
let prior_turns = normalize_cached_channel_turns(prior_turns_raw);
let mut history = vec![ChatMessage::system(ctx.system_prompt.as_str())];
history.append(&mut prior_turns);
if let Some(instructions) = channel_delivery_instructions(&msg.channel) {
history.push(ChatMessage::system(instructions));
}
let system_prompt = build_channel_system_prompt(ctx.system_prompt.as_str(), &msg.channel);
let mut history = vec![ChatMessage::system(system_prompt)];
history.extend(prior_turns);
let use_streaming = target_channel
.as_ref()
.is_some_and(|ch| ch.supports_draft_updates());
@ -3994,6 +4034,87 @@ mod tests {
assert!(calls[1][3].1.contains("follow up"));
}
#[tokio::test]
async fn process_channel_message_telegram_keeps_system_instruction_at_top_only() {
let channel_impl = Arc::new(TelegramRecordingChannel::default());
let channel: Arc<dyn Channel> = channel_impl.clone();
let mut channels_by_name = HashMap::new();
channels_by_name.insert(channel.name().to_string(), channel);
let provider_impl = Arc::new(HistoryCaptureProvider::default());
let mut histories = HashMap::new();
histories.insert(
"telegram_alice".to_string(),
vec![
ChatMessage::assistant("stale assistant"),
ChatMessage::user("earlier user question"),
ChatMessage::assistant("earlier assistant reply"),
],
);
let runtime_ctx = Arc::new(ChannelRuntimeContext {
channels_by_name: Arc::new(channels_by_name),
provider: provider_impl.clone(),
default_provider: Arc::new("test-provider".to_string()),
memory: Arc::new(NoopMemory),
tools_registry: Arc::new(vec![]),
observer: Arc::new(NoopObserver),
system_prompt: Arc::new("test-system-prompt".to_string()),
model: Arc::new("test-model".to_string()),
temperature: 0.0,
auto_save_memory: false,
max_tool_iterations: 5,
min_relevance_score: 0.0,
conversation_histories: Arc::new(Mutex::new(histories)),
provider_cache: Arc::new(Mutex::new(HashMap::new())),
route_overrides: Arc::new(Mutex::new(HashMap::new())),
api_key: None,
api_url: None,
reliability: Arc::new(crate::config::ReliabilityConfig::default()),
provider_runtime_options: providers::ProviderRuntimeOptions::default(),
workspace_dir: Arc::new(std::env::temp_dir()),
message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS,
interrupt_on_new_message: false,
multimodal: crate::config::MultimodalConfig::default(),
});
process_channel_message(
runtime_ctx.clone(),
traits::ChannelMessage {
id: "tg-msg-1".to_string(),
sender: "alice".to_string(),
reply_target: "chat-telegram".to_string(),
content: "hello".to_string(),
channel: "telegram".to_string(),
timestamp: 1,
thread_ts: None,
},
CancellationToken::new(),
)
.await;
let calls = provider_impl
.calls
.lock()
.unwrap_or_else(|e| e.into_inner());
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].len(), 4);
let roles = calls[0]
.iter()
.map(|(role, _)| role.as_str())
.collect::<Vec<_>>();
assert_eq!(roles, vec!["system", "user", "assistant", "user"]);
assert!(
calls[0][0]
.1
.contains("When responding on Telegram, include media markers"),
"telegram delivery instruction should live in the system prompt"
);
assert!(!calls[0].iter().skip(1).any(|(role, _)| role == "system"));
}
// ── AIEOS Identity Tests (Issue #168) ─────────────────────────
#[test]