memoryd: Tiers listing; bxctl memory tiers

Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
2026-10-03 17:54:18 -07:00
co-authored by ornith-1.5-35b-a3b
parent 0386c343f2
commit 3bea6c24c9
11 changed files with 365 additions and 2 deletions
+1
View File
@@ -12,3 +12,4 @@ pub mod recall;
pub mod review;
pub mod serve;
pub mod socket;
pub mod tiers;
+1
View File
@@ -62,6 +62,7 @@ impl Memoryd {
Some(mirror) => mirror.report(),
None => report::NOT_CONFIGURED.to_string(),
}),
MemoryCommand::Tiers => crate::tiers::list(&self.store, now),
};
if result.is_ok() && matches!(command, MemoryCommand::Undo | MemoryCommand::Accept) {
self.mirror(None);
+143
View File
@@ -0,0 +1,143 @@
//! The tiering listing: identity entries and notes with recall-hit counts (M5d spec, 4.4).
use std::collections::{HashMap, HashSet};
use std::io::ErrorKind;
use bxcore::{MemoryFile, Timestamp};
use serde::Deserialize;
use crate::files::Store;
use crate::notes::{self, Kind, Note};
#[derive(Deserialize)]
struct Logged {
time: String,
hits: Vec<Hit>,
}
#[derive(Deserialize)]
struct Hit {
kind: String,
session: String,
record: i64,
}
/// How many recall lines named one note, and the latest of those lines' times.
#[derive(Default)]
struct Tally {
n: u64,
last: String,
}
fn fail(path: &std::path::Path, e: std::io::Error) -> String {
format!("{}: {e}", path.display())
}
fn class_of(note: &Note) -> &'static str {
match &note.kind {
Kind::Noted { class, .. } => notes::class_name(*class),
Kind::Moved(_) | Kind::Dropped(_) => "private",
}
}
fn trust_of(note: &Note) -> &'static str {
match &note.kind {
Kind::Noted { untrusted, .. } => {
if *untrusted {
"untrusted"
} else {
"trusted"
}
}
Kind::Moved(_) | Kind::Dropped(_) => "trusted",
}
}
fn read_notes(store: &Store) -> Result<String, String> {
let path = store.path("notes.md");
match std::fs::read_to_string(&path) {
Ok(text) => Ok(text),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(String::new()),
Err(e) => Err(fail(&path, e)),
}
}
/// The recall hits per `(session, record)`: how many lines named it and the latest of their times
/// (M5d spec, section 4.4). A missing `recalls/` is an empty map; a line that does not parse is
/// skipped, and a line counts once for each note it names.
fn tally(store: &Store) -> Result<HashMap<(String, i64), Tally>, String> {
let dir = store.path("recalls");
let files = match std::fs::read_dir(&dir) {
Ok(files) => files,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(HashMap::new()),
Err(e) => return Err(fail(&dir, e)),
};
let mut hits: HashMap<(String, i64), Tally> = HashMap::new();
for file in files {
let file = file.map_err(|e| fail(&dir, e))?;
if !file.file_type().map_err(|e| fail(&dir, e))?.is_file() {
continue;
}
let path = file.path();
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) if e.kind() == ErrorKind::NotFound => continue,
Err(e) => return Err(fail(&path, e)),
};
for line in text.lines() {
let logged: Logged = match serde_json::from_str(line) {
Ok(logged) => logged,
Err(_) => continue,
};
let mut seen: HashSet<(String, i64)> = HashSet::new();
for hit in &logged.hits {
if hit.kind == "noted" {
seen.insert((hit.session.clone(), hit.record));
}
}
for key in seen {
let tally = hits.entry(key).or_default();
tally.n += 1;
if logged.time > tally.last {
tally.last = logged.time.clone();
}
}
}
}
Ok(hits)
}
/// One listing for `bxctl memory tiers` and for the tiering job's user text.
pub fn list(store: &Store, _now: Timestamp) -> Result<String, String> {
let mut out = String::new();
for file in [MemoryFile::Memory, MemoryFile::User] {
for entry in store.entries(file)? {
out.push_str(&format!(
"identity {} hits 0 last -\n {}\n",
file.name(),
entry
));
}
}
let hits = tally(store)?;
for (i, note) in notes::parse(&read_notes(store)?).into_iter().enumerate() {
let key = (note.session.clone(), i as i64);
let (n, last) = match hits.get(&key) {
Some(t) => (t.n, t.last.clone()),
None => (0, "-".to_string()),
};
out.push_str(&format!(
"note session {} hits {} last {} {} {}\n {}\n",
note.session,
n,
last,
class_of(&note),
trust_of(&note),
note.text
));
}
Ok(out)
}
+122
View File
@@ -0,0 +1,122 @@
//! Tests for `MemoryCommand::Tiers`: identity entries and notes with recall-hit counts. Do not edit.
#[path = "support/tmp.rs"]
mod tmp;
use bxcore::{MemoryFile, Timestamp};
use memoryd::files::Store;
use memoryd::tiers;
use tmp::TempDir;
fn now() -> Timestamp {
Timestamp::parse("2026-10-03T08:00:00.000Z").unwrap()
}
struct Setup {
_dir: TempDir,
store: Store,
}
fn setup(tag: &str) -> Setup {
let dir = TempDir::new(tag);
let starter = dir.write("etc/soul.md", "Soul.\n");
let store = Store::new(&dir.path().join("memory"));
store.init(&starter).unwrap();
Setup { _dir: dir, store }
}
fn write_recall(store: &Store, line: &str) {
std::fs::create_dir_all(store.path("recalls")).unwrap();
store
.append("recalls/2026-10.jsonl", &format!("{line}\n"))
.unwrap();
}
#[test]
fn identity_entries_list_with_zero_hits() {
let s = setup("tiers-id");
s.store
.write_entries(MemoryFile::Memory, &["I like tea.".to_string()])
.unwrap();
s.store
.write_entries(MemoryFile::User, &["The owner is Kyle.".to_string()])
.unwrap();
let text = tiers::list(&s.store, now()).unwrap();
assert!(
text.contains("identity memory.md hits 0 last -"),
"{text}"
);
assert!(text.contains("I like tea."), "{text}");
assert!(text.contains("identity user.md hits 0 last -"), "{text}");
assert!(text.contains("The owner is Kyle."), "{text}");
}
#[test]
fn notes_carry_counts_from_recalls() {
let s = setup("tiers-notes");
std::fs::write(
s.store.path("notes.md"),
"§ noted 2026-09-22T18:20Z in session mm-a · private\nlaunch window moved.\n",
)
.unwrap();
write_recall(
&s.store,
r#"{"time":"2026-10-03T07:00:00.000Z","session":"mm-now","query":"launch","hits":[{"kind":"noted","session":"mm-a","epoch":0,"record":0}]}"#,
);
write_recall(
&s.store,
r#"{"time":"2026-10-03T07:30:00.000Z","session":"mm-now","query":"window","hits":[{"kind":"noted","session":"mm-a","epoch":0,"record":0}]}"#,
);
let text = tiers::list(&s.store, now()).unwrap();
assert!(
text.contains(
"note session mm-a hits 2 last 2026-10-03T07:30:00.000Z private trusted"
),
"{text}"
);
assert!(text.contains("launch window moved."), "{text}");
}
#[test]
fn untrusted_notes_are_listed_and_marked() {
let s = setup("tiers-untrusted");
std::fs::write(
s.store.path("notes.md"),
"§ noted 2026-09-22T18:20Z in session mm-c · private · untrusted\nexample.org says the launch moved.\n",
)
.unwrap();
let text = tiers::list(&s.store, now()).unwrap();
assert!(
text.contains("note session mm-c hits 0 last - private untrusted"),
"{text}"
);
assert!(
text.contains("example.org says the launch moved."),
"{text}"
);
}
#[test]
fn a_recall_of_a_different_note_does_not_count() {
let s = setup("tiers-other");
std::fs::write(
s.store.path("notes.md"),
"§ noted 2026-09-22T18:20Z in session mm-a · private\nfirst note.\n§ noted 2026-09-22T18:21Z in session mm-b · private\nsecond note.\n",
)
.unwrap();
write_recall(
&s.store,
r#"{"time":"2026-10-03T07:00:00.000Z","session":"mm-now","query":"second","hits":[{"kind":"noted","session":"mm-b","epoch":0,"record":1}]}"#,
);
let text = tiers::list(&s.store, now()).unwrap();
assert!(
text.contains("note session mm-a hits 0 last - private trusted"),
"{text}"
);
assert!(
text.contains(
"note session mm-b hits 1 last 2026-10-03T07:00:00.000Z private trusted"
),
"{text}"
);
}