1use std::path::Path;
15
16use crate::open_loops;
17use crate::timeline;
18use crate::LAYER_NAMES;
19
20pub const TIMELINE_TOP_N: usize = 5;
22pub const LOOPS_TOP_N: usize = 5;
23pub const CORE_MEMORIES_TOP_N: usize = 3;
25pub const CORE_MEMORIES_MIN_IMP: u8 = 7;
27pub const CORE_MEMORY_MAX_CHARS: usize = 500;
29
30#[derive(Debug, Clone, Default)]
32pub struct CoreMemory {
33 pub text: String,
34 pub layer: String,
35 pub importance: u8,
36 pub depth: u8,
37}
38
39#[derive(Debug, Clone, Default)]
41pub struct AutoContext {
42 pub timeline_entries: Vec<timeline::TimelineEntry>,
43 pub open_loops: Vec<open_loops::OpenLoopEntry>,
44 pub block_count: usize,
45 pub depth_breakdown: Vec<(u8, usize)>,
46 pub append_log_size: usize,
47 pub last_session_anchor_ms: Option<u64>,
48 pub core_memories: Vec<CoreMemory>,
50}
51
52pub fn build(output_dir: &Path, reader: &crate::reader::MicroscopeReader) -> AutoContext {
54 let mut ctx = AutoContext::default();
55
56 let entries = timeline::read_all(&output_dir.join("timeline.bin"));
58 let filtered = timeline::filter(&entries, &timeline::TimeWindow::LastSession);
59 let mut rev: Vec<timeline::TimelineEntry> = filtered.iter().rev().cloned().collect();
60 rev.truncate(TIMELINE_TOP_N);
61 ctx.last_session_anchor_ms = filtered.first().map(|e| e.ts_ms);
62 ctx.timeline_entries = rev;
63
64 let mut open = open_loops::read_open(&output_dir.join("open_loops.bin"));
66 open.truncate(LOOPS_TOP_N);
67 ctx.open_loops = open;
68
69 ctx.block_count = reader.block_count;
71 for (d, &(_start, count)) in reader.depth_ranges.iter().enumerate() {
72 if count > 0 {
73 ctx.depth_breakdown.push((d as u8, count as usize));
74 }
75 }
76
77 let append_path = output_dir.join("append.bin");
79 if append_path.exists() {
80 if let Ok(meta) = std::fs::metadata(&append_path) {
81 const AVG_ENTRY: u64 = 147;
83 let total = meta.len();
84 ctx.append_log_size = (total / AVG_ENTRY) as usize;
85 }
86 }
87
88 let appended = crate::reader::read_append_log(&append_path);
91 let mut candidates: Vec<CoreMemory> = appended
92 .iter()
93 .filter(|e| e.importance >= CORE_MEMORIES_MIN_IMP)
94 .map(|e| {
95 let layer = LAYER_NAMES
96 .get(e.layer_id as usize)
97 .unwrap_or(&"?")
98 .to_string();
99 CoreMemory {
100 text: crate::safe_truncate(&e.text, CORE_MEMORY_MAX_CHARS).to_string(),
101 layer,
102 importance: e.importance,
103 depth: 0,
104 }
105 })
106 .collect();
107 candidates.sort_by_key(|a| std::cmp::Reverse(a.importance));
109 candidates.truncate(CORE_MEMORIES_TOP_N);
110 ctx.core_memories = candidates;
111
112 ctx
113}
114
115pub const AUTO_CONTEXT_HEADER: &str = "[AUTO-CONTEXT]";
117
118pub fn render(ctx: &AutoContext) -> String {
120 let mut out = String::new();
121 out.push_str(&format!(
122 "{}\n╔═══════════════════════════════════════════════════════╗\n",
123 AUTO_CONTEXT_HEADER
124 ));
125
126 out.push_str(&format!(
128 "║ STATE: {} blocks | {} depths active | append log ~{} entries\n",
129 ctx.block_count,
130 ctx.depth_breakdown.len(),
131 ctx.append_log_size,
132 ));
133 if ctx.last_session_anchor_ms.is_some() {
134 out.push_str(&format!(
135 "║ SESSION STARTED: {}\n",
136 ctx.last_session_anchor_ms
137 .map(timeline::format_ts)
138 .unwrap_or_else(|| "?".to_string())
139 ));
140 }
141
142 if !ctx.timeline_entries.is_empty() {
144 out.push_str("║\n║ LAST SESSION TIMELINE (newest first):\n");
145 for e in &ctx.timeline_entries {
146 let layer = LAYER_NAMES.get(e.layer_id as usize).unwrap_or(&"?");
147 let status_label = match e.status {
148 timeline::STATUS_OPEN => "OPEN",
149 timeline::STATUS_RESOLVED => "RESOLVED",
150 timeline::STATUS_ARCHIVED => "ARCHIVED",
151 _ => "",
152 };
153 let status_str = if status_label.is_empty() {
154 String::new()
155 } else {
156 format!(" [{}]", status_label)
157 };
158 out.push_str(&format!(
159 "║ {} {} D{} [{}] imp={}{} {}\n",
160 timeline::format_ts(e.ts_ms),
161 "",
162 e.depth,
163 layer,
164 e.importance,
165 status_str,
166 crate::safe_truncate(&e.text, 90),
167 ));
168 }
169 } else {
170 out.push_str("║ LAST SESSION: empty (no stores yet)\n");
171 }
172
173 if !ctx.open_loops.is_empty() {
175 out.push_str(&format!(
176 "║\n║ OPEN LOOPS ({} pending):\n",
177 ctx.open_loops.len()
178 ));
179 for l in &ctx.open_loops {
180 out.push_str(&format!(
181 "║ #{} {} imp={} {}\n",
182 l.id,
183 timeline::format_ts(l.ts_ms),
184 l.importance,
185 crate::safe_truncate(&l.text, 100),
186 ));
187 }
188 } else {
189 out.push_str("║ OPEN LOOPS: none\n");
190 }
191
192 if !ctx.core_memories.is_empty() {
194 out.push_str(&format!(
195 "║\n║ CORE MEMORIES (imp>={}, full text):\n",
196 CORE_MEMORIES_MIN_IMP
197 ));
198 for (i, m) in ctx.core_memories.iter().enumerate() {
199 out.push_str(&format!(
200 "║ [{}/{}] [{}] imp={} >>>\n",
201 i + 1,
202 ctx.core_memories.len(),
203 m.layer,
204 m.importance,
205 ));
206 for line in m.text.lines() {
208 out.push_str(&format!("║ {}\n", line));
209 }
210 out.push_str("║ <<<\n");
211 }
212 }
213
214 out.push_str("╚═══════════════════════════════════════════════════════╝\n");
215 out
216}
217
218pub fn render_compact(ctx: &AutoContext) -> String {
220 let mut out = String::new();
221 if !ctx.timeline_entries.is_empty() {
222 out.push_str(&format!(
223 "{} last session: {} stores.\n",
224 AUTO_CONTEXT_HEADER,
225 ctx.timeline_entries.len()
226 ));
227 for e in ctx.timeline_entries.iter().take(3) {
228 let layer = LAYER_NAMES.get(e.layer_id as usize).unwrap_or(&"?");
229 let status_label = match e.status {
230 timeline::STATUS_OPEN => "[OPEN]",
231 timeline::STATUS_RESOLVED => "[RESOLVED]",
232 _ => "",
233 };
234 out.push_str(&format!(
235 " • {} [{}] imp={} {} {}\n",
236 timeline::format_ts(e.ts_ms),
237 layer,
238 e.importance,
239 status_label,
240 crate::safe_truncate(&e.text, 70),
241 ));
242 }
243 }
244 if !ctx.open_loops.is_empty() {
245 out.push_str(&format!(
246 "{} {} open loop{}:\n",
247 AUTO_CONTEXT_HEADER,
248 ctx.open_loops.len(),
249 if ctx.open_loops.len() == 1 { "" } else { "s" }
250 ));
251 for l in ctx.open_loops.iter().take(3) {
252 out.push_str(&format!(
253 " • #{} imp={} {}\n",
254 l.id,
255 l.importance,
256 crate::safe_truncate(&l.text, 80),
257 ));
258 }
259 }
260 out
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn test_render_empty() {
269 let ctx = AutoContext::default();
270 let s = render(&ctx);
271 assert!(s.contains(AUTO_CONTEXT_HEADER));
272 assert!(s.contains("OPEN LOOPS: none"));
273 assert!(s.contains("empty"));
274 }
275
276 #[test]
277 fn test_render_compact_includes_timeline_and_loops() {
278 let mut ctx = AutoContext::default();
279 ctx.timeline_entries.push(timeline::TimelineEntry {
280 ts_ms: 1_700_000_000_000,
281 layer_id: 1,
282 importance: 7,
283 depth: 4,
284 status: timeline::STATUS_NORMAL,
285 text: "compacted".to_string(),
286 });
287 ctx.open_loops.push(open_loops::OpenLoopEntry {
288 id: 1,
289 ts_ms: 1_700_000_000_000,
290 importance: 8,
291 status: open_loops::STATUS_OPEN,
292 text: "todo".to_string(),
293 });
294 let s = render_compact(&ctx);
295 assert!(s.contains("compacted"));
296 assert!(s.contains("#1"));
297 assert!(s.contains("todo"));
298 }
299}