Plan M5a: local memory in 15 tasks
memoryd (files, the check, edits, journal, index, recall, serving), the memory tools and built-in grants in brokerd, the memory block in loopd, bxctl reindex and memory, and the device checks with memoryd. Each task was built as a reference and gated. Each was replayed on its own at its end state (854 to 936 tests) and mutation-checked. Each skeleton compiles and fails its tests. Every todo comment was read beside its signature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
//! `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<MemoryFile> {
|
||||
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<Note> {
|
||||
// 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!()
|
||||
}
|
||||
Reference in New Issue
Block a user