# M5d-2 task 10: `MemoryCommand::Tiers` and `bxctl memory tiers` **Branch:** `m5d2`. Check it: `git branch --show-current` prints `m5d2` and `git status --short` is empty; otherwise stop. Never switch branch. **Commit subject:** `memoryd: Tiers listing; bxctl memory tiers` **Log row:** the Task cell is `M5d2/10-memoryd-tiers`. ## Goal `MemoryCommand::Tiers` lists each identity entry and each note with how many times `recalls/` named it and when it was last found (spec 4.4). Untrusted notes are included and marked. Identity entries list with 0 hits (README known limits). `bxctl memory tiers` prints that listing. The starter `config/jobs/tier.toml.example` documents a weekly job; nothing in `loopd` special-cases it. ## Every place that changes | Place | What happens there | |---|---| | `crates/bxcore/src/memory.rs` | `MemoryCommand::Tiers` after `Sync` | | `crates/memoryd/src/tiers.rs` | new: `list` | | `crates/memoryd/src/lib.rs` | `pub mod tiers;` | | `crates/memoryd/src/serve/admin.rs` | `Tiers` arm | | `crates/bxctl/src/memory.rs` | `["tiers"]` | | `crates/bxctl/src/cli.rs` | `USAGE` names `tiers` | | `config/jobs/tier.toml.example` | new, given starter | | `crates/memoryd/tests/tiers.rs` | new, given | | `crates/bxctl/tests/cli_memory_tiers.rs` | new, given | | everything else | **no change**: not `cli_memory.rs` (T19: new file) | ## Files - Copy, from `docs/plans/M5d2/files/` (the same path under it): the new tests `crates/memoryd/tests/tiers.rs` and `crates/bxctl/tests/cli_memory_tiers.rs`, and the starter `config/jobs/tier.toml.example`. - Create: `crates/memoryd/src/tiers.rs` (function below). - Modify: `crates/bxcore/src/memory.rs`, `crates/memoryd/src/lib.rs`, `crates/memoryd/src/serve/admin.rs`, `crates/bxctl/src/memory.rs`, `crates/bxctl/src/cli.rs`, `docs/implementer-log.md`. - Do not touch: any other file. Do not edit `crates/bxctl/tests/cli_memory.rs`. ## Facts you need - **`MemoryCommand` is `rename_all = "snake_case"`.** JSON is `"tiers"`. Add the variant after `Sync`, with a doc comment. `admin.rs` match is exhaustive: add the arm in this task or it does not compile. - **Listing format, exact.** One block per identity entry, then one per note, in file order: ```text identity memory.md hits 0 last - I like tea. identity user.md hits 0 last - The owner is Kyle. note session mm-a hits 2 last 2026-10-03T07:30:00.000Z private trusted launch window moved. note session mm-c hits 0 last - private untrusted example.org says the launch moved. ``` Two spaces before the text line. `hits` and `last` for identity are always `0` and `-`. For a note, `hits` is how many `recalls/*.jsonl` lines contain a hit with `kind == "noted"`, `session == note.session`, `record ==` that note's index in `notes::parse` (from 0). `last` is the maximum `time` string among those lines, or `-`. Class is `public` / `private` / `secret` (`notes::class_name`). Trust is `untrusted` when `Kind::Noted { untrusted: true }`, else `trusted`. Moved/dropped notes: still list them as `note` using the same line, class `private`, `trusted` (they have no untrusted flag). - **`recalls/`.** `store.path("recalls")`. Missing directory: all counts 0. A file that exists but cannot be read is an error. Ignore a line that does not parse. Deserialize: ```rust #[derive(Deserialize)] struct Logged { time: String, hits: Vec, } #[derive(Deserialize)] struct Hit { kind: String, session: String, record: i64, } ``` (extra fields in the file are ignored: this is our format but old lines may grow; the given tests use exactly those fields plus `session` / `query` / `epoch` on the line. Use `deny_unknown_fields` **off** so `epoch` and `query` do not fail.) - **Identity.** `store.entries(MemoryFile::Memory)` then `User`. Empty file: no identity lines for that file. - **`list` returns `Result`.** Trailing newline on the last block is fine; `bxctl` adds another when printing `MemoryAdminReply.text` (`writeln!`). The given bxctl test includes a trailing newline in the reply text and expects it plus `writeln!`'s newline. - **`admin` arm.** `MemoryCommand::Tiers => tiers::list(&self.store, now)` — `now` is unused by `list` today; keep the signature `list(store, now)` so the given tests compile. - **`bxctl`.** `["tiers"] => Ask::Admin(MemoryCommand::Tiers, String::new())`. `["tiers", _]` is usage. USAGE: add `| tiers` on the `bxctl memory` line. - rustfmt `mod` order in `memoryd/src/lib.rs`. - The items the edits use. This table is complete. | Item | What it does | |---|---| | `Store::entries` | identity | | `notes::parse` / `class_name` | notes | | `Store::path("recalls")` | the jsonl directory | | `MemoryFile::name` | `memory.md` / `user.md` | ## The function `crates/memoryd/src/tiers.rs` — fill `list`. No `todo!()`. ```rust //! The tiering listing: identity entries and notes with recall-hit counts (M5d spec, 4.4). use bxcore::{MemoryFile, Timestamp}; use serde::Deserialize; use crate::files::Store; use crate::notes::{self, Kind}; #[derive(Deserialize)] struct Logged { time: String, hits: Vec, } #[derive(Deserialize)] struct Hit { kind: String, session: String, record: i64, } /// One listing for `bxctl memory tiers` and for the tiering job's user text. pub fn list(store: &Store, _now: Timestamp) -> Result { let _ = store; Ok(String::new()) } ``` Replace the stub body in this task. ### `crates/bxcore/src/memory.rs` **M1.** After `Sync,` in `MemoryCommand`: ```rust /// Identity entries and notes with recall-hit counts (M5d spec, section 4.4). Tiers, ``` ### `crates/memoryd/src/serve/admin.rs` **A1.** After the `Sync` arm, before the closing of the `match`: ```rust MemoryCommand::Tiers => crate::tiers::list(&self.store, now), ``` ### `crates/bxctl/src/memory.rs` **B1.** After `["sync"] => ...`, before `_ => return Err(UsageError)`: ```rust ["tiers"] => Ask::Admin(MemoryCommand::Tiers, String::new()), ``` ### `crates/bxctl/src/cli.rs` **U1.** The `bxctl memory` USAGE line. The line ```text bxctl memory log [N] | undo | pending | accept | reject | sync [--memory-socket PATH] ``` becomes ```text bxctl memory log [N] | undo | pending | accept | reject | sync | tiers [--memory-socket PATH] ``` ## Steps - [ ] **1. Copy the given files.** ```sh cp docs/plans/M5d2/files/crates/memoryd/tests/tiers.rs crates/memoryd/tests/ cp docs/plans/M5d2/files/crates/bxctl/tests/cli_memory_tiers.rs crates/bxctl/tests/ mkdir -p config/jobs cp docs/plans/M5d2/files/config/jobs/tier.toml.example config/jobs/tier.toml.example ``` - [ ] **2. See them fail.** `cargo build --tests --keep-going -p bxcore -p memoryd -p bxctl`, timeout 600000 ms: `tiers` does not compile (`E0432` unresolved `memoryd::tiers`); `cli_memory_tiers` does not compile (`E0599` no variant `Tiers`). After M1 alone, `admin.rs` fails (`non-exhaustive patterns`). Do M1, A1, `tiers.rs`, B1, U1 together in step 3. This step is the first build against the given tests. - [ ] **3. The edits.** M1, `tiers.rs` with a real `list`, `pub mod tiers;`, A1, B1, U1. `cargo check -p bxcore -p memoryd -p bxctl` prints no error and no warning. `wc -l crates/memoryd/src/tiers.rs` prints a number ≤ 500. - [ ] **4. The tests.** `cargo test -p memoryd --test tiers` (4) and `cargo test -p bxctl --test cli_memory_tiers --test cli_memory --test cli_memory_sync` (2 new): every test passes. - [ ] **5. Check.** `git diff --stat` lists the files in "Every place that changes" except the new tests and starter (those are untracked until `git add`). - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line `gate: ok`. Test count **1574** (1568 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d2/10-memoryd-tiers`. Stage with `git add crates/bxcore crates/memoryd crates/bxctl config/jobs docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when - `make gate` prints `gate: ok` with 1574 tests. - `git diff HEAD~1 --stat` lists only the files this task names plus the log. ## Stop and report if - The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo check` warns; a test fails; the gate count is not 1574; you edit `cli_memory.rs`; identity entries show a non-zero hit count; untrusted notes are omitted; you add a cron crate; you special-case the name `tier` in `loopd`. ## What the given tests pin Identity lines are `hits 0 last -` with the entry text; notes join `recalls/` on `(noted, session, record)` and take the latest `time`; untrusted notes are listed and marked; a hit on note 1 does not count for note 0; `bxctl memory tiers` parses and prints the reply.