diff --git a/poc-memory/src/agents/defs.rs b/poc-memory/src/agents/defs.rs index a3cd7f4..ab0dc82 100644 --- a/poc-memory/src/agents/defs.rs +++ b/poc-memory/src/agents/defs.rs @@ -416,10 +416,118 @@ fn resolve( }) } + // conversation — tail of the current session transcript (post-compaction) + "conversation" => { + let text = resolve_conversation(); + if text.is_empty() { None } + else { Some(Resolved { text, keys: vec![] }) } + } + + // seen_recent — recently surfaced memory keys for this session + "seen_recent" => { + let text = resolve_seen_recent(); + Some(Resolved { text, keys: vec![] }) + } + _ => None, } } +/// Get the tail of the current session's conversation. +/// Reads POC_SESSION_ID to find the transcript, extracts the last +/// segment (post-compaction), returns the tail (~100K chars). +fn resolve_conversation() -> String { + let session_id = std::env::var("POC_SESSION_ID").unwrap_or_default(); + if session_id.is_empty() { return String::new(); } + + let projects = crate::config::get().projects_dir.clone(); + // Find the transcript file matching this session + let mut transcript = None; + if let Ok(dirs) = std::fs::read_dir(&projects) { + for dir in dirs.filter_map(|e| e.ok()) { + let path = dir.path().join(format!("{}.jsonl", session_id)); + if path.exists() { + transcript = Some(path); + break; + } + } + } + + let Some(path) = transcript else { return String::new() }; + let path_str = path.to_string_lossy(); + let messages = match super::enrich::extract_conversation(&path_str) { + Ok(m) => m, + Err(_) => return String::new(), + }; + + // Take the last segment (post-compaction) + let segments = super::enrich::split_on_compaction(messages); + let Some(segment) = segments.last() else { return String::new() }; + + // Format and take the tail + let cfg = crate::config::get(); + let mut text = String::new(); + for (_, role, content, ts) in segment { + let name = if role == "user" { &cfg.user_name } else { &cfg.assistant_name }; + if !ts.is_empty() { + text.push_str(&format!("**{}** {}: {}\n\n", name, &ts[..ts.len().min(19)], content)); + } else { + text.push_str(&format!("**{}:** {}\n\n", name, content)); + } + } + + // Tail: keep last ~100K chars + const MAX_CHARS: usize = 100_000; + if text.len() > MAX_CHARS { + // Find a clean line break near the cut point + let start = text.len() - MAX_CHARS; + let start = text[start..].find('\n').map(|i| start + i + 1).unwrap_or(start); + text[start..].to_string() + } else { + text + } +} + +/// Get recently surfaced memory keys for the current session. +fn resolve_seen_recent() -> String { + let session_id = std::env::var("POC_SESSION_ID").unwrap_or_default(); + if session_id.is_empty() { + return "(no session ID — cannot load seen set)".to_string(); + } + + let state_dir = std::path::PathBuf::from("/tmp/claude-memory-search"); + let mut entries: Vec<(String, String)> = Vec::new(); + + for suffix in ["", "-prev"] { + let path = state_dir.join(format!("seen{}-{}", suffix, session_id)); + if let Ok(content) = std::fs::read_to_string(&path) { + entries.extend( + content.lines() + .filter(|s| !s.is_empty()) + .filter_map(|line| { + let (ts, key) = line.split_once('\t')?; + Some((ts.to_string(), key.to_string())) + }) + ); + } + } + + if entries.is_empty() { + return "(no memories surfaced yet this session)".to_string(); + } + + // Sort newest first, dedup + entries.sort_by(|a, b| b.0.cmp(&a.0)); + let mut seen = std::collections::HashSet::new(); + let recent: Vec = entries.into_iter() + .filter(|(_, key)| seen.insert(key.clone())) + .take(20) + .map(|(ts, key)| format!("- {} (surfaced {})", key, ts)) + .collect(); + + recent.join("\n") +} + /// Resolve all {{placeholder}} patterns in a prompt template. /// Returns the resolved text and all node keys collected from placeholders. pub fn resolve_placeholders(