PicoBot/src/gateway/compaction.rs
oudecheng 4baa8e7a6b fix: 内存历史按 topic_id 键化,新增 replace_topic_history 修复 DB 压缩覆盖
- SessionHistory.chat_histories 改为 topic_histories 按 topic_id 键化,消除多话题并发时内存历史互相覆盖的根因

- is_current_turn 改用 current_topic == original_topic_id 直接比较,替代失效的内存最新消息匹配判断

- 新增 ConversationRepository::replace_topic_history 按 topic 删除/插入消息,解决 replace_active_history 删除整个 session 消息的 pre-existing 问题

- compaction 路径改用 replace_topic_history,避免压缩时覆盖其他 topic 的 DB 消息

- switch_topic 不再 remove_history,不同 topic 历史独立存储互斥

- append_persisted_message 仅当 topic_id == current_topic 时更新内存,防止延迟消息污染新话题历史
2026-07-28 15:32:42 +08:00

72 lines
2.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::sync::Arc;
use tokio::sync::Mutex;
use crate::agent::AgentError;
use super::session::Session;
/// Run two-segment history compression synchronously.
///
/// Unlike the previous background approach (tokio::spawn), this holds the
/// session lock during the LLM calls (25 seconds). Since the agent loop
/// has already finished by this point there is no response-time impact, and
/// the synchronous guarantee means the next execution always starts with
/// freshly compacted history.
///
/// 按 topic_id 隔离:压缩只处理指定 topic 的历史DB 替换也只影响该 topic。
pub(crate) async fn schedule_background_history_compaction(
session: Arc<Mutex<Session>>,
chat_id: impl Into<String>,
topic_id: impl Into<String>,
) -> Result<(), AgentError> {
let chat_id = chat_id.into();
let topic_id = topic_id.into();
let mut session_guard = session.lock().await;
session_guard.ensure_persistent_session(&chat_id)?;
session_guard.ensure_chat_loaded(&chat_id, Some(&topic_id))?;
let history = session_guard.get_or_create_history(&topic_id).clone();
let compressor = session_guard.compressor().clone();
if !compressor.should_compress(&history) {
return Ok(());
}
let store = session_guard.store();
let session_id = session_guard.persistent_session_id(&chat_id);
let provider_config = session_guard.provider_config().clone();
tracing::info!(
chat_id = %chat_id,
topic_id = %topic_id,
msg_count = history.len(),
"Starting synchronous two-segment compression"
);
// Synchronous compression — holds lock during LLM calls.
// compress_two_segment guarantees the result contains no tool_calls,
// so there is no risk of orphaned tool call sequences.
let compressed = compressor
.compress_two_segment(&history, &provider_config)
.await?;
// Replace only this topic's history in DB (not the entire session).
// This avoids clobbering other topics' messages during compaction.
store
.replace_topic_history(&session_id, &topic_id, &compressed)
.map_err(|e| AgentError::Other(format!("replace_topic_history error: {}", e)))?;
tracing::info!(
chat_id = %chat_id,
topic_id = %topic_id,
compressed_msg_count = compressed.len(),
"Two-segment compression committed"
);
session_guard.reload_topic_history(&chat_id, &topic_id)?;
Ok(())
}