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

9.7 KiB

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:

    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:

    #[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!().

//! 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:

    /// 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:

            MemoryCommand::Tiers => crate::tiers::list(&self.store, now),

crates/bxctl/src/memory.rs

B1. After ["sync"] => ..., before _ => return Err(UsageError):

        ["tiers"] => Ask::Admin(MemoryCommand::Tiers, String::new()),

crates/bxctl/src/cli.rs

U1. The bxctl memory USAGE line. The line

  bxctl memory log [N] | undo <id> | pending | accept <id> | reject <id> | sync [--memory-socket PATH]

becomes

  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

        USAGE.contains("| reject <id> | sync [--memory-socket PATH]\n"),
        "{USAGE}"

become

        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.

    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.