//! `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 proto::{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 { // `time.to_rfc3339()` is like 2026-09-24T20:14:37.123Z. Keep its first 16 characters // (`get(..16)`, or the whole string if it is shorter) and add "Z": 2026-09-24T20:14Z. todo!() } 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 { // Noted: format!("§ noted {time} in session {session} · {class}") with " · untrusted" added // when untrusted; the class is written by `class_name`. Moved: format!("§ moved {time} from // {file} in session {session}"), the file as `file.name()`. Dropped: the same with // "dropped". todo!() } /// 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): the header, the text, a final newline. todo!() } } /// Read a header line back, or `None` when the line is not one. fn parse_header(line: &str) -> Option<(Kind, String, String)> { // `None` whenever the line does not fit exactly. // 1. `strip_prefix("§ ")?`, then `split_once(' ')?` for the word, then again for the time. // 2. "noted": the rest must start "in session "; split what follows on " · ": the session, then // the class ("public", "private" or "secret", anything else None), then optionally // "untrusted" (anything else None), then nothing more. The session must not be empty or // contain a space. Some((Kind::Noted { class, untrusted }, time, session)). // 3. "moved" or "dropped": the rest must start "from "; `split_once(' ')?` gives the file name, // which `file_of` must know; what follows must start "in session " and the session must not // be empty or contain a space. Some((Kind::Moved(file) or Kind::Dropped(file), time, // session)). // 4. Any other word: None. todo!() } /// 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 { // For each line of `text.lines()`: when `parse_header(line)` is Some, push a new Note with an // empty text; otherwise, when there is a note above, add the line to its text (a newline before // it unless its text is still empty). Lines before the first header are dropped. At the end, // trim() the text of every note. todo!() }