feat: enhance memory management tools; improve lesson tracking and update descriptions for clarity

This commit is contained in:
asepharyana
2026-07-12 12:21:46 +07:00
parent 40108defc0
commit ce36e936a6
4 changed files with 153 additions and 25 deletions
+91 -11
View File
@@ -130,6 +130,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.push_transcript(ChatMessageDisplay::new(Role::User, text.clone()));
if let Some(ref mut rt) = state.session_runtime {
rt.push_message(ChatMessage::user(text));
refresh_lesson_counters(&state.memory_dir, rt);
} else {
let _ = std::fs::create_dir_all(&state.memory_dir);
}
state.misc.thinking = true;
spawn_turn(state);
@@ -372,19 +375,15 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
let _ = trigger_review(state);
}
} else if kind == "review" {
let lessons_found = if message.contains("lesson") || message.contains("Lesson") {
message.rsplit(' ').next().and_then(|w| {
w.trim_end_matches(')').trim_end_matches('s')
.split('(').next_back()
.and_then(|n| n.parse::<u32>().ok())
}).unwrap_or(0)
let counted = if let Some(ref mut rt) = state.session_runtime {
refresh_lesson_counters(&state.memory_dir, rt);
true
} else {
0
false
};
if let Some(ref mut rt) = state.session_runtime {
if lessons_found > 0 {
if counted {
rt.consecutive_empty_reviews = 0;
rt.lesson_count += lessons_found;
} else {
rt.consecutive_empty_reviews += 1;
}
@@ -634,6 +633,85 @@ fn generate_workspace_tree(roots: &[std::path::PathBuf]) -> String {
out
}
/// Load all memory entries from `memory_dir` and format them as a compact
/// section appended to the system prompt, so the AI is always aware of
/// stored lessons and project knowledge.
///
/// Flow: list memory slugs → for each, read + parse the file → collect
/// entries whose lifecycle is not "stale" → cap total output at 3000 chars
/// to avoid dominating the prompt budget.
///
/// Why: previously, lessons existed on disk but the AI never saw them
/// unless it explicitly called `recall()`. This makes the memory system
/// actually useful by surfacing relevant knowledge automatically.
///
/// Return: a formatted string (may be empty if no memory entries exist).
fn build_memory_section(memory_dir: &std::path::Path) -> String {
let names = crate::model::memory::Memory::list(memory_dir);
if names.is_empty() {
return String::new();
}
let mut section = String::from("\n\n--- Persistent Memory ---\n");
section.push_str(&format!("Total entries: {}\n\n", names.len()));
for name in &names {
if section.len() > 3000 {
section.push_str("... (more entries omitted, use recall() to see all)\n");
break;
}
if let Ok(mem) = crate::model::memory::Memory::read(memory_dir, name) {
if mem.lifecycle == "stale" {
continue;
}
section.push_str(&format!("## [{}] {}\n{}\n\n", mem.kind, mem.name, mem.content));
}
}
section.push_str("---");
section
}
/// Scan `memory_dir` and update every lesson counter in `SessionRuntime`
/// from real on-disk data.
///
/// Flow: list all memory slugs → read+parse each → increment the matching
/// kind counter (user/feedback/project/reference), lifecycle counter
/// (active/stale/contradicted), and the total. If a memory cannot be read
/// (e.g. a race with deletion) it is silently skipped.
///
/// Why: previously the UI showed all zeros because nothing ever set the
/// breakdown counters. This runs on every user submit so the dashboard
/// reflects actual memory state.
fn refresh_lesson_counters(memory_dir: &std::path::Path, rt: &mut crate::app::state::runtime::SessionRuntime) {
let names = crate::model::memory::Memory::list(memory_dir);
rt.lesson_count = 0;
rt.lessons_user = 0;
rt.lessons_feedback = 0;
rt.lessons_project = 0;
rt.lessons_reference = 0;
rt.lessons_active = 0;
rt.lessons_stale = 0;
rt.lessons_contradicted = 0;
for name in &names {
if let Ok(mem) = crate::model::memory::Memory::read(memory_dir, name) {
rt.lesson_count += 1;
match mem.kind.as_str() {
"user" => rt.lessons_user += 1,
"feedback" => rt.lessons_feedback += 1,
"project" => rt.lessons_project += 1,
"reference" => rt.lessons_reference += 1,
_ => {}
}
match mem.lifecycle.as_str() {
"active" => rt.lessons_active += 1,
"stale" => rt.lessons_stale += 1,
"contradicted" => rt.lessons_contradicted += 1,
_ => {}
}
}
}
}
/// Persist a `ChatMessage` to the SQLite message log, if a database
/// connection is available.
///
@@ -679,11 +757,13 @@ fn run_agent_turn(
let mut prev_shaped = false;
let tree_info = generate_workspace_tree(&tc.workspace_roots);
let memory_section = build_memory_section(&tc.ctx.memory_dir);
let system_text = format!(
"{}\n\n{}\n\n{}",
"{}\n\n{}\n\n{}{}",
crate::resources::SYSTEM_PROMPT,
crate::resources::SYSTEM_TOOLS,
tree_info
tree_info,
memory_section,
);
if !msgs.iter().any(|m| matches!(m.role, crate::dto::chat::message::Role::System)) {
let sys = ChatMessage::system(system_text);