feat(knowledge_review): query past sessions for cross-session patterns

Background review now queries the SQLite session history via
search_chat_history before running the extraction. Recent sessions
(up to 3, excluding current) are summarized and appended to the
review prompt as context.

This lets the review agent notice cross-session patterns:
- 'user corrected me about X in multiple sessions'
- 'this tool quirk keeps coming up'
- 'user always prefers Y approach'

Uses the existing ChatHistorySearch infrastructure — no new
dependencies or schema changes needed.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-04-14 11:28:43 +10:00
parent 8cd03525b5
commit 53cbd97387
2 changed files with 73 additions and 3 deletions
+1
View File
@@ -1825,6 +1825,7 @@ impl Agent {
super::knowledge_review::spawn_background_review(
provider,
Arc::clone(&self.extension_manager),
Arc::clone(&self.config.session_manager),
conversation.clone(),
session_config.id.clone(),
working_dir.clone(),
+72 -3
View File
@@ -74,16 +74,18 @@ const FLUSH_PROMPT: &str = "[System: The session context is being compressed. Sa
/// Spawn a background task to review the conversation for memory and/or skill saves.
///
/// Runs AFTER the reply is delivered. The user never sees this.
#[allow(clippy::too_many_arguments)]
pub fn spawn_background_review(
provider: Arc<dyn Provider>,
extension_manager: Arc<ExtensionManager>,
session_manager: Arc<crate::session::SessionManager>,
conversation: Conversation,
session_id: String,
working_dir: std::path::PathBuf,
review_memory: bool,
review_skills: bool,
) {
let prompt = if review_memory && review_skills {
let base_prompt = if review_memory && review_skills {
COMBINED_REVIEW_PROMPT
} else if review_skills {
SKILL_REVIEW_PROMPT
@@ -99,14 +101,26 @@ pub fn spawn_background_review(
"memory_review"
};
let sid = session_id.clone();
tokio::spawn(async move {
// Query past sessions for cross-session patterns
let session_context = build_session_context(&session_manager, &sid).await;
let prompt = if session_context.is_empty() {
base_prompt.to_string()
} else {
format!(
"{}\n\nContext from past sessions (consider saving recurring patterns):\n{}",
base_prompt, session_context
)
};
if let Err(e) = run_knowledge_extraction(
provider.as_ref(),
&extension_manager,
&conversation,
&session_id,
&sid,
&working_dir,
prompt,
&prompt,
task_name,
ReviewScope {
include_memory_tools: review_memory,
@@ -120,6 +134,61 @@ pub fn spawn_background_review(
});
}
/// Query recent past sessions for recurring patterns to feed into the review.
///
/// Extracts key user messages from recent sessions to help the review agent
/// notice cross-session patterns (e.g. "user always does X", "this tool quirk
/// keeps coming up").
async fn build_session_context(
session_manager: &crate::session::SessionManager,
exclude_session_id: &str,
) -> String {
use crate::session::session_manager::SessionType;
// Search recent sessions for user messages (broad query)
let results = session_manager
.search_chat_history(
"*", // broad search
Some(5),
None,
None,
Some(exclude_session_id.to_string()),
vec![SessionType::User],
)
.await;
let Ok(results) = results else {
return String::new();
};
if results.results.is_empty() {
return String::new();
}
let mut context = String::new();
for session in results.results.iter().take(3) {
if session.messages.is_empty() {
continue;
}
context.push_str(&format!(
"\n[Session: {} ({})]\n",
session.session_description, session.last_activity
));
for msg in session.messages.iter().take(5) {
let preview: String = msg.content.chars().take(200).collect();
context.push_str(&format!(" {}: {}\n", msg.role, preview));
}
}
// Cap at 2000 chars to avoid bloating the review prompt
if context.len() > 2000 {
context.truncate(2000);
context.push_str("\n[...truncated]");
}
context
}
/// Run the pre-compression flush synchronously before compaction.
pub async fn flush_memories_before_compaction(
provider: &dyn Provider,