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