Files
boxmaker/docs/plans/M5d1/01-loopd-compact-config.md
kyleandGrok 4.6 3726ae585f docs: M5d-1 and M5d-2 implementation plans for Ornith
Eight closed tasks for epochs and compaction, then ten for the scheduler,
ToolSummary, and tiers. Given tests first. Gate 1456 → 1507 → 1572.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-02 21:54:37 -07:00

210 lines
7.9 KiB
Markdown

# M5d-1 task 01: `[compact]` on `loopd.toml`
**Branch:** `m5d1`. Check it: `git branch --show-current` prints `m5d1` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `loopd: [compact] threshold, summary_chars and summarise`
**Log row:** the Task cell is `M5d1/01-loopd-compact-config`.
## Goal
M5d-1 compact after a turn whose last `Usage` is at or above a fraction of `expect.n_ctx` (spec
3.3, 5). This task adds only the `[compact]` table so later tasks can read it:
- `threshold` (float, default `0.85`)
- `summary_chars` (default `4000`)
- `summarise` (list of tool names, default `["http_fetch"]`)
Nothing in this task uses the values. `summarise` is stored so a spec-complete `loopd.toml`
loads; M5d-2 reads it. Unknown keys in `[compact]` are errors, as in every table we define.
`Config::compact_prompt` is `compact.md` beside `[baseline] system` (the same directory join
`load` already does for `system.md`). The starter file is copied in task 05.
## Every place that changes
This is the complete list.
| Place | What happens there |
|---|---|
| `crates/loopd/src/config.rs` | `Compact`; `Config.compact`; `Config::compact_prompt` (edits C1 to C3) |
| `crates/loopd/tests/config_compact.rs` | new, given |
| everything else | **no change**: not `session.rs`, not the existing `config.rs` tests or fixtures |
## Files
- Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test
`crates/loopd/tests/config_compact.rs`.
- Modify: `crates/loopd/src/config.rs`, `docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **`Config` already has `#[serde(default)]` on optional tables** (`sampling`, `limits`, `paths`,
`channel`, `loop`, `baseline`, `broker`, `memory`). `[compact]` is the same: missing means
`Compact::default()`.
- **`threshold` is `f64`.** `Sampling.temperature` is already `f64` on `Config`, so `PartialEq`
on `Config` still compiles. Do not add a range check: the spec does not name one.
- **`summarise` default.** `vec!["http_fetch".to_string()]`. An explicit `summarise = []` is
allowed and is empty. Unknown names are not checked here (M5d-2).
- **`compact_prompt`.** `self.baseline.system.with_file_name("compact.md")`. After `load`,
`baseline.system` is already joined to the config file's directory, so the prompt follows it
into a subdirectory when `system` does (`prompts/agent.md` → `prompts/compact.md`).
- The items the edits use. This table is complete; `serde` is already a dependency of `loopd`.
| Item | What it does |
|---|---|
| `#[serde(deny_unknown_fields, default)]` | on `Compact`: missing table uses `Default`; unknown keys error |
| `Path::with_file_name` | `compact.md` beside `system.md` |
| `Vec<String>` | `summarise` |
## The edits, exactly
Nothing else in this file changes. Each quoted "before" block occurs exactly once in its file
when its turn comes (make them in order). Keep the fields in the order shown.
### `crates/loopd/src/config.rs`
**C1.** After the `Memory` struct (the lines)
```rust
pub struct Memory {
pub socket: std::path::PathBuf,
}
```
become
```rust
pub struct Memory {
pub socket: std::path::PathBuf,
}
/// `[compact]`: when a session starts a new epoch, and how long a summary may be (M5d spec,
/// section 5). `summarise` is stored here and unused until M5d-2.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Compact {
pub threshold: f64,
pub summary_chars: usize,
pub summarise: Vec<String>,
}
impl Default for Compact {
fn default() -> Self {
Self {
threshold: 0.85,
summary_chars: 4_000,
summarise: vec!["http_fetch".to_string()],
}
}
}
```
**C2.** The lines
```rust
#[serde(default)]
pub memory: Memory,
}
```
become
```rust
#[serde(default)]
pub memory: Memory,
#[serde(default)]
pub compact: Compact,
}
```
**C3.** After `channel_socket` (the lines)
```rust
pub fn channel_socket(&self) -> std::path::PathBuf {
if self.channel.socket.as_os_str().is_empty() {
self.paths.home.join("run/loop/loop.sock")
} else {
self.channel.socket.clone()
}
}
}
```
become
```rust
pub fn channel_socket(&self) -> std::path::PathBuf {
if self.channel.socket.as_os_str().is_empty() {
self.paths.home.join("run/loop/loop.sock")
} else {
self.channel.socket.clone()
}
}
/// `compact.md` beside the system prompt: the summary call's last user message.
pub fn compact_prompt(&self) -> std::path::PathBuf {
self.baseline.system.with_file_name("compact.md")
}
}
```
## Steps
- [ ] **1. Copy the given test.** From the repository root, one command:
```sh
cp docs/plans/M5d1/files/crates/loopd/tests/config_compact.rs crates/loopd/tests/
```
Then `git status --short` lists one new file, `crates/loopd/tests/config_compact.rs`.
- [ ] **2. See it fail.** `cargo build --tests --keep-going -p loopd`, with a shell timeout of
600000 ms: one test target, `config_compact`, does not compile, with `E0609` (`no field
\`compact\` on type \`Config\``) and `E0599` (`no method named \`compact_prompt\`` /
`no variant or associated item named \`default\` for \`Compact\``). Every other target
compiles.
- [ ] **3. The edits.** Make edits C1 to C3, in order. `cargo check -p loopd` prints no error and
no warning.
- [ ] **4. The tests.** `cargo test -p loopd --test config_compact --test config`: every test
passes, 8 in `config_compact` and the others as before.
- [ ] **5. Check.** `git diff --stat` lists exactly `crates/loopd/src/config.rs`.
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, with a shell timeout of 600000 ms.
Expected last line: `gate: ok`. The test count is **1464** after this task, 1456 before it
(1455 passed in `cargo test --workspace`, plus 1), and the 8 new ones are the tests of
`config_compact.rs`. To count, add up the `N passed` of every `test result:` line the gate's
`cargo test --workspace` printed, and add 1. Never run the gate again only to count.
- [ ] **7. Log and commit.** Add your row to `docs/implementer-log.md`; its Task cell is
`M5d1/01-loopd-compact-config`. Stage with
`git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says.
`Cargo.lock` does not change in this task; the `git add` is harmless.
## Done when
- `make gate` prints `gate: ok` with 1464 tests.
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/config.rs`,
`crates/loopd/tests/config_compact.rs` and `docs/implementer-log.md`.
## Stop and report if
- The branch check at the top fails.
- A "before" block is not in its file exactly as shown when its turn comes, or is there more
than once.
- Step 2 shows a different result: another target failing, or other errors.
- `cargo check` in step 3 prints an error or a warning. Quote it, and stop.
- A test fails in step 4. Do not change the test: stop, and give the failing tests' names and
their messages.
- The fix seems to need a change anywhere else: a fixture, `config.rs` tests, a range check on
`threshold`. It does not; stop and say why you think so.
- A test outside `config_compact.rs` fails in the gate: check once whether it fails without your
change (`git stash -u`, run it, `git stash pop`), then stop and report both results.
- The gate passes with a count other than 1464.
- A comment in an edit does not fit the code under it. Quote it and stop.
## What the given tests pin
Each of these changes to the finished code makes at least one given test fail: the default
threshold moved; `summary_chars` default moved; `summarise` default emptied or given another
name; unknown keys accepted; a misspelt `summary_char` accepted; a partial table wiping the
other defaults; `compact_prompt` using `home` or the config file's name instead of `system.md`'s
directory; `Compact::default` disagreeing with a missing table.