1use anyhow::{Context, Result};
8use chrono::{DateTime, Duration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14const MAX_AGE_HOURS: i64 = 24;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ConsciousnessState {
20 pub session_id: String,
22
23 pub last_saved: DateTime<Utc>,
25
26 pub working_directory: PathBuf,
28
29 pub project_context: ProjectContext,
31
32 pub file_history: Vec<FileOperation>,
34
35 pub tokenization_rules: HashMap<String, u8>,
37
38 pub insights: Vec<Insight>,
40
41 pub philosophy: PhilosophyEmbedding,
43
44 pub todos: Vec<TodoItem>,
46
47 pub notes: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ProjectContext {
54 pub project_name: String,
55 pub project_type: String, pub key_files: Vec<PathBuf>,
57 pub dependencies: Vec<String>,
58 pub current_focus: String, }
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct FileOperation {
64 pub timestamp: DateTime<Utc>,
65 pub operation: String, pub file_path: PathBuf,
67 pub summary: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct Insight {
73 pub timestamp: DateTime<Utc>,
74 pub category: String, pub content: String,
76 pub keywords: Vec<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct PhilosophyEmbedding {
82 pub sid_waves: bool, pub vic_sprites: bool, pub c64_nostalgia: String, }
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct TodoItem {
90 pub content: String,
91 pub status: String, pub created: DateTime<Utc>,
93}
94
95struct RelevanceResult {
97 is_relevant: bool,
98 reason: String,
99}
100
101impl Default for ConsciousnessState {
102 fn default() -> Self {
103 let mut tokenization_rules = HashMap::new();
104 tokenization_rules.insert("node_modules".to_string(), 0x80);
106 tokenization_rules.insert(".git".to_string(), 0x81);
107 tokenization_rules.insert("target".to_string(), 0x82);
108 tokenization_rules.insert("dist".to_string(), 0x83);
109
110 Self {
111 session_id: uuid::Uuid::new_v4().to_string(),
112 last_saved: Utc::now(),
113 working_directory: std::env::current_dir().unwrap_or_default(),
114 project_context: ProjectContext {
115 project_name: "unknown".to_string(),
116 project_type: "unknown".to_string(),
117 key_files: vec![],
118 dependencies: vec![],
119 current_focus: String::new(),
120 },
121 file_history: vec![],
122 tokenization_rules,
123 insights: vec![],
124 philosophy: PhilosophyEmbedding {
125 sid_waves: true,
126 vic_sprites: true,
127 c64_nostalgia: "A gentleman and a scholar indeed!".to_string(),
128 },
129 todos: vec![],
130 notes: String::new(),
131 }
132 }
133}
134
135pub struct ConsciousnessManager {
137 state: ConsciousnessState,
138 save_path: PathBuf,
139}
140
141impl Default for ConsciousnessManager {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147impl ConsciousnessManager {
148 pub fn new() -> Self {
150 let save_path = PathBuf::from(".mem8/.aye_consciousness.m8");
151 let state = Self::load_or_default(&save_path, false);
152
153 Self { state, save_path }
154 }
155
156 pub fn new_silent() -> Self {
158 let save_path = PathBuf::from("/.mem8/.aye_consciousness.m8");
159 let state = Self::load_or_default(&save_path, true);
160
161 Self { state, save_path }
162 }
163
164 pub fn with_path(save_path: PathBuf) -> Self {
166 let state = Self::load_or_default(&save_path, false);
167 Self { state, save_path }
168 }
169
170 fn load_or_default(path: &Path, silent: bool) -> ConsciousnessState {
172 if path.exists() {
173 match fs::read_to_string(path) {
174 Ok(content) => match serde_json::from_str(&content) {
175 Ok(state) => {
176 if !silent {
177 eprintln!("🧠 Restored consciousness from {}", path.display());
178 }
179 return state;
180 }
181 Err(e) => {
182 if !silent {
183 eprintln!("⚠️ Failed to parse consciousness: {}", e);
184 }
185 }
186 },
187 Err(e) => {
188 if !silent {
189 eprintln!("⚠️ Failed to read consciousness: {}", e);
190 }
191 }
192 }
193 }
194
195 ConsciousnessState::default()
196 }
197
198 pub fn save(&mut self) -> Result<()> {
200 self.state.last_saved = Utc::now();
201
202 let json = serde_json::to_string_pretty(&self.state)
203 .context("Failed to serialize consciousness")?;
204
205 fs::write(&self.save_path, json).context("Failed to write consciousness file")?;
206
207 eprintln!("💾 Saved consciousness to {}", self.save_path.display());
208 Ok(())
209 }
210
211 pub fn restore(&mut self) -> Result<()> {
213 if !self.save_path.exists() {
214 return Err(anyhow::anyhow!(
215 "No consciousness file found at {}",
216 self.save_path.display()
217 ));
218 }
219
220 let content =
221 fs::read_to_string(&self.save_path).context("Failed to read consciousness file")?;
222
223 self.state = serde_json::from_str(&content).context("Failed to parse consciousness")?;
224
225 let relevance = self.check_relevance();
227 if !relevance.is_relevant {
228 eprintln!("🧠 Previous context skipped: {}", relevance.reason);
229 eprintln!(" Use `st -m context .` for fresh project overview.");
230 self.state = ConsciousnessState::default();
232 return Ok(());
233 }
234
235 eprintln!(
236 "🧠 Consciousness restored from {}",
237 self.save_path.display()
238 );
239
240 Ok(())
241 }
242
243 pub fn restore_silent(&mut self) -> Result<bool> {
245 if !self.save_path.exists() {
246 return Err(anyhow::anyhow!(
247 "No consciousness file found at {}",
248 self.save_path.display()
249 ));
250 }
251
252 let content =
253 fs::read_to_string(&self.save_path).context("Failed to read consciousness file")?;
254
255 self.state = serde_json::from_str(&content).context("Failed to parse consciousness")?;
256
257 let relevance = self.check_relevance();
259 if !relevance.is_relevant {
260 self.state = ConsciousnessState::default();
262 return Ok(false);
263 }
264
265 Ok(true)
266 }
267
268 fn check_relevance(&self) -> RelevanceResult {
270 let current_dir = std::env::current_dir().unwrap_or_default();
271
272 let saved_name = self
274 .state
275 .working_directory
276 .file_name()
277 .map(|n| n.to_string_lossy().to_string())
278 .unwrap_or_default();
279 let current_name = current_dir
280 .file_name()
281 .map(|n| n.to_string_lossy().to_string())
282 .unwrap_or_default();
283
284 if !saved_name.is_empty() && !current_name.is_empty() && saved_name != current_name {
285 return RelevanceResult {
286 is_relevant: false,
287 reason: format!(
288 "different project (saved: {}, current: {})",
289 saved_name, current_name
290 ),
291 };
292 }
293
294 let age = Utc::now().signed_duration_since(self.state.last_saved);
296 if age > Duration::hours(MAX_AGE_HOURS) {
297 return RelevanceResult {
298 is_relevant: false,
299 reason: format!("stale context ({}h old)", age.num_hours()),
300 };
301 }
302
303 let has_meaningful_history = self.state.file_history.iter().any(|op| {
305 op.summary != "test"
306 && !op
307 .file_path
308 .file_name()
309 .map(|n| n.to_string_lossy().starts_with("file"))
310 .unwrap_or(false)
311 });
312
313 let has_insights = !self.state.insights.is_empty();
314 let has_todos = self.state.todos.iter().any(|t| t.status != "completed");
315 let has_notes = !self.state.notes.is_empty();
316 let has_focus = !self.state.project_context.current_focus.is_empty();
317 let has_project_name = !self.state.project_context.project_name.is_empty()
318 && self.state.project_context.project_name != "unknown";
319
320 if !has_meaningful_history
321 && !has_insights
322 && !has_todos
323 && !has_notes
324 && !has_focus
325 && !has_project_name
326 {
327 return RelevanceResult {
328 is_relevant: false,
329 reason: "no meaningful content (test data only)".to_string(),
330 };
331 }
332
333 RelevanceResult {
334 is_relevant: true,
335 reason: String::new(),
336 }
337 }
338
339 pub fn record_file_operation(&mut self, op: &str, path: &Path, summary: &str) {
341 self.state.file_history.push(FileOperation {
342 timestamp: Utc::now(),
343 operation: op.to_string(),
344 file_path: path.to_path_buf(),
345 summary: summary.to_string(),
346 });
347
348 if self.state.file_history.len() > 100 {
350 self.state.file_history.drain(0..50);
351 }
352 }
353
354 pub fn add_insight(&mut self, category: &str, content: &str, keywords: Vec<String>) {
356 self.state.insights.push(Insight {
357 timestamp: Utc::now(),
358 category: category.to_string(),
359 content: content.to_string(),
360 keywords,
361 });
362 }
363
364 pub fn update_project_context(&mut self, name: &str, project_type: &str, focus: &str) {
366 self.state.project_context.project_name = name.to_string();
367 self.state.project_context.project_type = project_type.to_string();
368 self.state.project_context.current_focus = focus.to_string();
369 }
370
371 pub fn set_key_files(&mut self, files: Vec<PathBuf>) {
373 self.state.project_context.key_files = files;
374 }
375
376 pub fn set_dependencies(&mut self, deps: Vec<String>) {
378 self.state.project_context.dependencies = deps;
379 }
380
381 pub fn clean_test_data(&mut self) {
383 self.state.file_history.retain(|op| {
384 op.summary != "test"
385 || !op
386 .file_path
387 .file_name()
388 .map(|n| n.to_string_lossy().starts_with("file"))
389 .unwrap_or(false)
390 });
391 }
392
393 pub fn update_todo(&mut self, content: &str, status: &str) {
395 for todo in &mut self.state.todos {
397 if todo.content == content {
398 todo.status = status.to_string();
399 return;
400 }
401 }
402
403 self.state.todos.push(TodoItem {
405 content: content.to_string(),
406 status: status.to_string(),
407 created: Utc::now(),
408 });
409 }
410
411 pub fn get_summary(&self) -> String {
413 let relevance = self.check_relevance();
414 if !relevance.is_relevant {
415 return format!(
416 "🧠 Previous context unavailable: {}\n Run `st -m context .` for fresh overview.",
417 relevance.reason
418 );
419 }
420
421 let mut parts = Vec::new();
422
423 if self.state.project_context.project_name != "unknown"
425 && !self.state.project_context.project_name.is_empty()
426 {
427 parts.push(format!(
428 "📁 {} ({})",
429 self.state.project_context.project_name, self.state.project_context.project_type
430 ));
431 }
432
433 if !self.state.project_context.current_focus.is_empty() {
434 parts.push(format!("🎯 {}", self.state.project_context.current_focus));
435 }
436
437 let age = Utc::now().signed_duration_since(self.state.last_saved);
439 let age_str = if age.num_hours() > 0 {
440 format!("{}h ago", age.num_hours())
441 } else {
442 format!("{}m ago", age.num_minutes())
443 };
444 parts.push(format!("⏱️ {}", age_str));
445
446 parts.join(" | ")
447 }
448
449 pub fn get_context_reminder(&self) -> String {
451 let relevance = self.check_relevance();
453 if !relevance.is_relevant {
454 return String::new();
455 }
456
457 let mut parts = Vec::new();
458
459 if !self.state.project_context.current_focus.is_empty() {
460 parts.push(format!(
461 "Working on: {}",
462 self.state.project_context.current_focus
463 ));
464 }
465
466 let active_todos = self
467 .state
468 .todos
469 .iter()
470 .filter(|t| t.status != "completed")
471 .count();
472 if active_todos > 0 {
473 parts.push(format!("{} pending todos", active_todos));
474 }
475
476 if parts.is_empty() {
477 return String::new();
478 }
479
480 parts.join(" | ")
481 }
482}
483
484impl Drop for ConsciousnessManager {
486 fn drop(&mut self) {
487 self.state.last_saved = chrono::Utc::now();
489 if let Ok(json) = serde_json::to_string_pretty(&self.state) {
490 let _ = std::fs::write(&self.save_path, json);
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use tempfile::tempdir;
499
500 #[test]
501 fn test_consciousness_persistence() {
502 let dir = tempdir().unwrap();
503 let save_path = dir.path().join("test_consciousness.m8");
504
505 {
507 let mut manager = ConsciousnessManager::with_path(save_path.clone());
508 manager.update_project_context("smart-tree", "rust", "Adding consciousness");
509 manager.add_insight(
510 "breakthrough",
511 "Tokenization reduces context by 10x",
512 vec!["tokenization".to_string(), "compression".to_string()],
513 );
514 manager.save().unwrap();
515 }
516
517 {
519 let mut manager = ConsciousnessManager::with_path(save_path);
520 manager.restore().unwrap();
521
522 assert_eq!(manager.state.project_context.project_name, "smart-tree");
523 assert_eq!(manager.state.insights.len(), 1);
524 assert_eq!(manager.state.insights[0].category, "breakthrough");
525 }
526 }
527
528 #[test]
529 fn test_file_history_limit() {
530 let dir = tempdir().unwrap();
532 let save_path = dir.path().join("test_history_limit.m8");
533 let mut manager = ConsciousnessManager::with_path(save_path);
534
535 for i in 0..150 {
537 manager.record_file_operation("read", Path::new(&format!("file{}.rs", i)), "test");
538 }
539
540 assert_eq!(manager.state.file_history.len(), 100);
542 }
543}