//! `notes.md`: experience, append-only. Each entry is a header line saying how it got there, //! then its text (M5a spec, section 3): //! //! ```text //! § noted 2026-09-24T21:02Z in session mm-dh45qg… · private · untrusted //! § moved 2026-09-24T20:14Z from memory.md in session mm-y73sxj… //! § dropped 2026-09-24T20:14Z from memory.md in session mm-y73sxj… //! ``` use bxcore::{DataClass, MemoryFile, Timestamp}; /// How an entry came into `notes.md`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Kind { /// Something seen or told, with the session's class and whether it had read untrusted /// content. Noted { class: DataClass, untrusted: bool }, /// Identity that `consolidate` took out of the prompt: still held true. Moved(MemoryFile), /// Identity the agent stopped holding true. Dropped(MemoryFile), } /// One entry of `notes.md`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Note { pub kind: Kind, /// `YYYY-MM-DDTHH:MMZ`. pub time: String, pub session: String, pub text: String, } /// A time as the notes show it: to the minute, in UTC. pub fn minute(time: Timestamp) -> String { let s = time.to_rfc3339(); format!("{}Z", s.get(..16).unwrap_or(&s)) } fn class_name(class: DataClass) -> &'static str { match class { DataClass::Public => "public", DataClass::Private => "private", DataClass::Secret => "secret", } } fn file_of(name: &str) -> Option { match name { "memory.md" => Some(MemoryFile::Memory), "user.md" => Some(MemoryFile::User), _ => None, } } impl Note { /// The header line, without its newline. pub fn header(&self) -> String { match &self.kind { Kind::Noted { class, untrusted } => { let mut s = format!( "§ noted {} in session {} · {}", self.time, self.session, class_name(*class) ); if *untrusted { s.push_str(" · untrusted"); } s } Kind::Moved(file) => { format!( "§ moved {} from {} in session {}", self.time, file.name(), self.session ) } Kind::Dropped(file) => { format!( "§ dropped {} from {} in session {}", self.time, file.name(), self.session ) } } } /// The header, the text and a final newline: what is appended to `notes.md`. pub fn render(&self) -> String { format!("{}\n{}\n", self.header(), self.text) } } /// Read a header line back, or `None` when the line is not one. fn parse_header(line: &str) -> Option<(Kind, String, String)> { let after = line.strip_prefix("§ ")?; let (word, rest) = after.split_once(' ')?; let (time, rest) = rest.split_once(' ')?; match word { "noted" => { let rest = rest.strip_prefix("in session ")?; let parts: Vec<&str> = rest.split(" · ").collect(); if parts.len() < 2 || parts.len() > 3 { return None; } let session = parts[0]; if session.is_empty() || session.contains(' ') { return None; } let class = match parts[1] { "public" => DataClass::Public, "private" => DataClass::Private, "secret" => DataClass::Secret, _ => return None, }; let untrusted = match parts.get(2) { None => false, Some(&"untrusted") => true, Some(_) => return None, }; Some(( Kind::Noted { class, untrusted }, time.to_string(), session.to_string(), )) } "moved" | "dropped" => { let rest = rest.strip_prefix("from ")?; let (file_name, after) = rest.split_once(' ')?; let file = file_of(file_name)?; let after = after.strip_prefix("in session ")?; if after.is_empty() || after.contains(' ') { return None; } let kind = match word { "moved" => Kind::Moved(file), _ => Kind::Dropped(file), }; Some((kind, time.to_string(), after.to_string())) } _ => None, } } /// The entries of `notes.md`. Text before the first header, or under a line that is not a /// header, belongs to the entry above it; the owner may have edited the file. pub fn parse(text: &str) -> Vec { let mut notes: Vec = Vec::new(); for line in text.lines() { if let Some((kind, time, session)) = parse_header(line) { notes.push(Note { kind, time, session, text: String::new(), }); } else if let Some(last) = notes.last_mut() { if last.text.is_empty() { last.text.push_str(line); } else { last.text.push('\n'); last.text.push_str(line); } } } for note in &mut notes { note.text = note.text.trim().to_string(); } notes }