# M5d-1 task 05: when to compact, and the fallback summary **Branch:** `m5d1`. Check it: `git branch --show-current` prints `m5d1` and `git status --short` is empty; otherwise stop. Never switch branch. **Commit subject:** `loopd: compaction threshold and the fallback summary` **Log row:** the Task cell is `M5d1/05-loopd-compact-due`. ## Goal A session is due to compact when the current epoch's last `Usage` has `cache_n + prompt_n` at or above `threshold × expect.n_ctx` (spec 3.3). There is no idle timer. If the summary call is context-full (task 06), the fallback is the epoch's `User` texts, newest last, cut to `summary_chars` on a character boundary (spec 3.4). This task is those two pure functions, the `compact` module, and the starter `config/compact.md`. No inference, no disk besides copying the starter. ## Every place that changes | Place | What happens there | |---|---| | `crates/loopd/src/compact.rs` | new: `due`, `fallback_summary`, `cap_chars` | | `crates/loopd/src/lib.rs` | `pub mod compact;` | | `config/compact.md` | new, given starter | | `crates/loopd/tests/compact_due.rs` | new, given | | everything else | **no change** | ## Files - Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test `crates/loopd/tests/compact_due.rs` and the starter `config/compact.md`. - Create: `crates/loopd/src/compact.rs` (skeleton below). - Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md`. - Do not touch: any other file. ## Facts you need - **Depth is saturating.** `cache_n.saturating_add(prompt_n) as f64 >= threshold * (n_ctx as f64)`. 0.85 × 262144 is 222822.4, so 222822 is not due and 222823 is. 0.85 × 1000 is 850 exactly. - **Cast.** `u64 as f64` is exact for these magnitudes (n_ctx is 262144). Do not use `as` on untrusted indexes; these are our own config and the server's counts. - **Fallback join.** `"\n\n"` between texts. Newest last: reverse the `User` contents, then join, then cap. - **Cap.** `s.chars().take(n).collect::()`. `n == 0` is empty. A multi-byte character is not split. - **`config/compact.md`** is copied like `config/system.md`. Task 06 reads it from `Config::compact_prompt`. - The items the edits use. This table is complete. | Item | What it does | |---|---| | `bxcore::Usage` | `cache_n`, `prompt_n` | | `LogRecord::User { content, .. }` | fallback texts | | `str::chars`, `Iterator::take` | the cap | ## Skeleton: `crates/loopd/src/compact.rs` Write this file, then fill the two `todo!()` bodies from their comments and delete the comments. Each comment is at most six lines. ```rust //! Compaction: when a session starts a new epoch, the summary call, and writing the next files //! (M5d spec, section 3). Later tasks fill the call and the write. use bxcore::{LogRecord, Usage}; /// True when `cache_n + prompt_n` is at or above `threshold * n_ctx`. pub fn due(usage: &Usage, threshold: f64, n_ctx: u64) -> bool { // saturating_add the two counts; compare as f64 to `threshold * (n_ctx as f64)`. todo!() } /// The epoch's `User` texts, newest last, joined with `"\n\n"`, then `cap_chars`. pub fn fallback_summary(records: &[LogRecord], summary_chars: usize) -> String { // Collect `&str` contents of every `User`, reverse, join `"\n\n"`, then `cap_chars`. todo!() } pub(crate) fn cap_chars(s: &str, n: usize) -> String { s.chars().take(n).collect() } ``` ### `crates/loopd/src/lib.rs` **L1.** After `pub mod channel;` add `pub mod compact;` (keep rustfmt's order; if rustfmt moves the line, accept that). ## Steps - [ ] **1. Copy the given files.** ```sh cp docs/plans/M5d1/files/crates/loopd/tests/compact_due.rs crates/loopd/tests/ cp docs/plans/M5d1/files/config/compact.md config/compact.md ``` - [ ] **2. Write the skeleton** at `crates/loopd/src/compact.rs` and make L1. See it fail: `cargo test -p loopd --test compact_due`, timeout 600000 ms: the two `due` tests and the three `fallback` tests panic on `todo!()` (or fail their asserts if you filled the bodies early). If the test target does not compile (`E0432` unresolved `loopd::compact`), the `mod` line is missing. - [ ] **3. Fill `due` and `fallback_summary`.** Delete each instruction comment. `cargo check -p loopd` prints no error and no warning. - [ ] **4. The tests.** `cargo test -p loopd --test compact_due`: 6 passed. - [ ] **5. Check.** `git diff --stat` lists `crates/loopd/src/lib.rs`, `crates/loopd/src/compact.rs`, `config/compact.md`. `git status --short` shows `compact_due.rs` as new. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line `gate: ok`. Test count **1489** (1483 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d1/05-loopd-compact-due`. Stage with `git add crates/loopd config/compact.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when - `make gate` prints `gate: ok` with 1489 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/lib.rs`, `crates/loopd/src/compact.rs`, `crates/loopd/tests/compact_due.rs`, `config/compact.md` and the log. ## Stop and report if - The branch check fails; step 2 fails to compile for a reason other than the missing module or `todo!()`; a test fails; the gate count is not 1489; you add an idle timer. There is none. ## What the given tests pin 849/1000 is not due, 850 is; 222822/262144 is not due, 222823 is; threshold 1.0 is the full window; fallback is newest-last with `"\n\n"`; the cap is in characters on a boundary; no users is empty.