Files
boxmaker/docs/plans/M5d2/10-memoryd-tiers.md
T
kyleandGrok 4.6 2eb0ffd6d7 docs: M5d-2: read only the implementer-log header
Task 06 opened the whole implementer log (~360 kB) after reading supporting
modules; OpenCode returned "Context size has been exceeded" and made no
commit. AGENTS.md and tasks 06-10 now say to read the first 12 lines only.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-03 14:42:59 -07:00

254 lines
9.7 KiB
Markdown

# 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` |
| `crates/bxctl/tests/cli_memory_sync.rs` | USAGE pin matches U1 (edit S1) |
| `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`, `crates/bxctl/tests/cli_memory_sync.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<Hit>,
}
#[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.
- **Counts map.** A nested `HashMap<(String, i64), (u64, String)>` trips clippy
`type_complexity` (`-D warnings`). Name a small `Tally { n: u64, last: String }` and
return `HashMap<(String, i64), Tally>`. Do not `#[allow]`.
- **`list` returns `Result<String, String>`.** 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<Hit>,
}
#[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<String, String> {
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 <id> | pending | accept <id> | reject <id> | sync [--memory-socket PATH]
```
becomes
```text
bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> | sync | tiers [--memory-socket PATH]
```
### `crates/bxctl/tests/cli_memory_sync.rs`
**S1.** `sync_parses` pins the USAGE line from M5c. U1 changes it. The lines
```rust
USAGE.contains("| reject <id> | sync [--memory-socket PATH]\n"),
"{USAGE}"
```
become
```rust
USAGE.contains("| reject <id> | sync | tiers [--memory-socket PATH]\n"),
"{USAGE}"
```
Do not edit `cli_memory.rs`. This file is the M5c pin of the same USAGE string.
## 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, S1.
`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). After `gate: ok`, do not add up
`test result:` lines.
- [ ] **7. Log and commit.** Task cell `M5d2/10-memoryd-tiers`. Read only the first 12 lines of
`docs/implementer-log.md`; insert the row under the header. 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's last line is not `gate: ok`; you edit `cli_memory.rs`; you leave
the `cli_memory_sync` USAGE pin on the old string; 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.