vtcode_core/persistent_memory/
batch.rs1use std::collections::HashSet;
12use std::path::Path;
13use std::sync::Arc;
14
15use anyhow::{Context, Result, bail};
16use tokio::sync::Semaphore;
17
18use super::{FactsInput, GroundedFactRecord, PersistentMemoryWriteReport, RuntimeAgentConfig, VTCodeConfig};
19
20const FACTS_PER_SESSION: usize = 32;
23
24#[derive(Debug, Clone, serde::Serialize)]
26pub struct BatchMemoryReport {
27 pub sessions_scanned: usize,
29 pub sessions_with_facts: usize,
31 pub candidate_facts: usize,
33 pub active_sessions_skipped: usize,
35 pub write_report: Option<PersistentMemoryWriteReport>,
37}
38
39pub async fn run_batch_memory_extraction(
47 runtime_config: &RuntimeAgentConfig,
48 vt_cfg: Option<&VTCodeConfig>,
49 workspace_root: &Path,
50 max_sessions: usize,
51 concurrency: usize,
52) -> Result<BatchMemoryReport> {
53 if max_sessions == 0 {
54 bail!("batch memory extraction requires max_sessions > 0");
55 }
56 let config = super::effective_generated_memory_config(vt_cfg);
57 if !config.enabled {
58 bail!(
59 "persistent memory generation is disabled; enable `[agent.persistent_memory] enabled` (and memories) first"
60 );
61 }
62
63 let summaries = vtcode_memory::recent_sessions(workspace_root, max_sessions);
64 let semaphore = Arc::new(Semaphore::new(concurrency.max(1)));
65 let mut handles = Vec::with_capacity(summaries.len());
66 for summary in summaries {
67 let permit = semaphore
68 .clone()
69 .acquire_owned()
70 .await
71 .context("batch memory extraction semaphore closed")?;
72 let workspace = workspace_root.to_path_buf();
73 let session_id = summary.session_id.clone();
74 let active = summary.status == "active";
75 handles.push(tokio::task::spawn_blocking(move || {
76 let _permit = permit;
77 match vtcode_memory::session_memory_facts(&workspace, &session_id, FACTS_PER_SESSION) {
78 Ok(facts) => (session_id, active, facts),
79 Err(error) => {
80 tracing::warn!(session_id = %session_id, error = %error, "batch memory: failed to read session facts; skipping");
81 (session_id, active, Vec::new())
82 }
83 }
84 }));
85 }
86
87 let mut seen: HashSet<String> = HashSet::new();
88 let mut candidates: Vec<GroundedFactRecord> = Vec::new();
89 let mut sessions_scanned = 0usize;
90 let mut sessions_with_facts = 0usize;
91 let mut active_sessions_skipped = 0usize;
92 for handle in handles {
93 let Ok((session_id, active, facts)) = handle.await else {
94 continue;
95 };
96 sessions_scanned += 1;
97 if active {
98 active_sessions_skipped += 1;
101 continue;
102 }
103 let mut contributed = false;
104 for fact in facts {
105 let normalized = super::normalize_whitespace(&fact.fact).to_ascii_lowercase();
106 if normalized.is_empty() || !seen.insert(normalized) {
107 continue;
108 }
109 candidates.push(GroundedFactRecord {
110 fact: fact.fact,
111 source: format!("session:{session_id}"),
112 });
113 contributed = true;
114 }
115 if contributed {
116 sessions_with_facts += 1;
117 }
118 }
119
120 let candidate_count = candidates.len();
121 let write_report = if candidate_count == 0 {
122 None
123 } else {
124 super::persist_memory_internal(
125 &config,
126 workspace_root,
127 Some(runtime_config),
128 vt_cfg,
129 FactsInput::Candidates(&candidates),
130 true,
131 false,
132 )
133 .await
134 .context("batch memory consolidation")?
135 };
136
137 Ok(BatchMemoryReport {
138 sessions_scanned,
139 sessions_with_facts,
140 candidate_facts: candidate_count,
141 active_sessions_skipped,
142 write_report,
143 })
144}
145
146pub fn batch_parameters(config: &crate::config::PersistentMemoryConfig) -> (usize, usize) {
148 let sessions = config.memories.batch_sessions.max(1);
149 let concurrency = config.memories.batch_concurrency.clamp(1, 16);
150 (sessions, concurrency)
151}
152
153pub fn batch_parameters_from_config(config: Option<&crate::config::PersistentMemoryConfig>) -> (usize, usize) {
156 match config {
157 Some(config) => batch_parameters(config),
158 None => batch_parameters(&crate::config::PersistentMemoryConfig::default()),
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn batch_parameters_clamp_degenerate_config() {
168 let mut config = crate::config::PersistentMemoryConfig::default();
169 config.memories.batch_sessions = 0;
170 config.memories.batch_concurrency = 999;
171 let (sessions, concurrency) = batch_parameters(&config);
172 assert_eq!(sessions, 1);
173 assert_eq!(concurrency, 16);
174 }
175
176 #[test]
177 fn batch_parameters_use_defaults() {
178 let config = crate::config::PersistentMemoryConfig::default();
179 let (sessions, concurrency) = batch_parameters(&config);
180 assert_eq!(sessions, 50);
181 assert_eq!(concurrency, 8);
182 }
183}