Given tests: Action::Turn (not To), send_turn of epoch 1, clippy on
compact_summary, jsonl-without-baseline, live turn after compact sends
the carry. send_turn maps Damaged to Local("log") as read_turn did.
Gate counts 1484 → 1508. Mutations recorded in the README.
Co-Authored-By: Grok 4.6 <noreply@x.ai>
131 lines
5.4 KiB
Markdown
131 lines
5.4 KiB
Markdown
# 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::<String>()`. `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 **1490** (1484 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 1490 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 1490; 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.
|