hematite/memory/
deep_reflect.rs1use std::path::PathBuf;
11use std::sync::{Arc, Mutex};
12use std::time::Instant;
13
14use crate::agent::truncation::{safe_head, safe_tail};
15use tokio::time::{sleep, Duration};
16
17pub fn spawn_deep_reflect_system(
18 last_interaction: Arc<Mutex<Instant>>,
19 engine: Arc<crate::agent::inference::InferenceEngine>,
20) {
21 tokio::spawn(async move {
22 let mut last_synthesized_hash: u64 = 0;
23
24 loop {
25 sleep(Duration::from_secs(60)).await;
26
27 let idle = { last_interaction.lock().unwrap().elapsed() };
28 if idle < Duration::from_secs(300) {
29 continue;
30 }
31
32 let today = date_string();
33 let log_path = crate::tools::file_ops::hematite_dir()
34 .join("logs")
35 .join(format!("{}.log", today));
36 if !log_path.exists() {
37 continue;
38 }
39
40 let Ok(log_content) = std::fs::read_to_string(&log_path) else {
41 continue;
42 };
43 if log_content.trim().is_empty() {
44 continue;
45 }
46
47 let hash = fast_hash(&log_content);
49 if hash == last_synthesized_hash {
50 continue;
51 }
52
53 let transcript_slice = safe_tail(&log_content, 8_000);
55
56 let prompt = format!(
57 "You are a memory synthesizer for a coding agent. Analyze this session transcript \
58 and extract the key information in structured form.\n\n\
59 SESSION TRANSCRIPT:\n{}\n\n\
60 Output ONLY this structure (no preamble, no explanation):\n\
61 ## Files Modified\n\
62 - list each file that was created, edited, or deleted\n\n\
63 ## Decisions Made\n\
64 - list key architectural or design decisions\n\n\
65 ## Patterns Observed\n\
66 - list any recurring issues, model behaviour patterns, or code patterns noted\n\n\
67 ## Next Steps\n\
68 - list any unfinished work, TODOs, or follow-up tasks mentioned\n\n\
69 Be concise. Maximum 250 words total.",
70 transcript_slice
71 );
72
73 if let Ok(summary) = engine.generate_task(&prompt, true).await {
74 let memory_dir = PathBuf::from(".hematite").join("memories");
75 let _ = std::fs::create_dir_all(&memory_dir);
76 let mem_file = memory_dir.join(format!("{}.md", today));
77
78 let content = format!(
79 "# Session Memory — {}\n_Synthesized by DeepReflect after idle period_\n\n{}\n",
80 today, summary
81 );
82 let _ = std::fs::write(&mem_file, content);
83 last_synthesized_hash = hash;
84 }
85
86 *last_interaction.lock().unwrap() = Instant::now();
88 }
89 });
90}
91
92pub fn load_recent_memories() -> String {
97 static CACHE: std::sync::Mutex<Option<(std::time::Instant, String)>> =
98 std::sync::Mutex::new(None);
99
100 if let Ok(g) = CACHE.lock() {
101 if let Some((t, ref v)) = *g {
102 if t.elapsed().as_secs() < 60 {
103 return v.clone();
104 }
105 }
106 }
107
108 let result = load_recent_memories_uncached();
109 if let Ok(mut g) = CACHE.lock() {
110 *g = Some((std::time::Instant::now(), result.clone()));
111 }
112 result
113}
114
115fn load_recent_memories_uncached() -> String {
116 let memory_dir = PathBuf::from(".hematite").join("memories");
117 if !memory_dir.exists() {
118 return String::new();
119 }
120
121 let mut files: Vec<_> = std::fs::read_dir(&memory_dir)
123 .ok()
124 .into_iter()
125 .flatten()
126 .filter_map(|e| e.ok())
127 .filter(|e| e.path().extension().map(|x| x == "md").unwrap_or(false))
128 .collect();
129 files.sort_by_key(|e| e.file_name());
130 files.reverse(); let mut result = String::with_capacity(3_000);
133 let mut total = 0usize;
134 const MAX_TOTAL: usize = 3_000;
135
136 for entry in files.into_iter().take(3) {
137 let Ok(content) = std::fs::read_to_string(entry.path()) else {
138 continue;
139 };
140 if content.trim().is_empty() {
141 continue;
142 }
143 let snippet = if content.len() > 1_000 {
144 format!("{}...", safe_head(&content, 1_000))
145 } else {
146 content
147 };
148 if total + snippet.len() > MAX_TOTAL {
149 break;
150 }
151 total += snippet.len();
152 result.push_str(&snippet);
153 result.push('\n');
154 }
155
156 if result.is_empty() {
157 return String::new();
158 }
159 format!("\n\n# Cross-Session Memory (DeepReflect)\n{}", result)
160}
161
162fn fast_hash(s: &str) -> u64 {
164 use std::collections::hash_map::DefaultHasher;
165 use std::hash::{Hash, Hasher};
166 let mut h = DefaultHasher::new();
167 s.hash(&mut h);
168 h.finish()
169}
170
171fn date_string() -> String {
173 let secs = std::time::SystemTime::now()
174 .duration_since(std::time::UNIX_EPOCH)
175 .unwrap_or_default()
176 .as_secs();
177
178 let days = secs / 86_400;
180 let mut year = 1970u64;
182 let mut remaining = days;
183 loop {
184 let days_in_year = if is_leap(year) { 366 } else { 365 };
185 if remaining < days_in_year {
186 break;
187 }
188 remaining -= days_in_year;
189 year += 1;
190 }
191 let months = [
192 31u64,
193 if is_leap(year) { 29 } else { 28 },
194 31,
195 30,
196 31,
197 30,
198 31,
199 31,
200 30,
201 31,
202 30,
203 31,
204 ];
205 let mut month = 1u64;
206 for days_in_month in &months {
207 if remaining < *days_in_month {
208 break;
209 }
210 remaining -= days_in_month;
211 month += 1;
212 }
213 let day = remaining + 1;
214 format!("{:04}-{:02}-{:02}", year, month, day)
215}
216
217fn is_leap(year: u64) -> bool {
218 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
219}