session_slot and the scheduler tests held a temporary JSON Value across statements (E0716). Cron now pins DOM and DOW as AND, which the given tests had not. Gate counts from task 04 on move up by one. Co-Authored-By: Grok 4.6 <noreply@x.ai>
228 lines
8.7 KiB
Markdown
228 lines
8.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` |
|
|
| `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<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.
|
|
- **`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]
|
|
```
|
|
|
|
## 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.
|