# M5d-1 task 06: the summary call, and `session-compact-failed` **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 summary on the background slot` **Log row:** the Task cell is `M5d1/06-loopd-compact-summary`. ## Goal Compaction asks for a summary of the current epoch on the **background** slot, with **no tools**, thinking on (spec 3.4). The last user message is `compact.md` (task 01's `compact_prompt`), trimmed at the end. The completion is tag-neutralised and cut to `summary_chars`. If that call is context-full (`exceed_context_size_error`), use task 05's `fallback_summary`. If `compact.md` cannot be read, or the call fails for any other reason, return `CompactError::Failed` whose `Display` ends with the runbook pointer `see docs/runbook.md#session-compact-failed`. There is no fallback for a missing prompt. This task does not write the next epoch. Task 07 does. ## Every place that changes | Place | What happens there | |---|---| | `crates/loopd/src/compact.rs` | `CompactError`; `summarise` (edits E1, E2) | | `docs/runbook.md` | append the given `session-compact-failed` entry | | `crates/loopd/tests/compact_summary.rs` | new, given | | everything else | **no change**: not `turn.rs`, not `channel.rs`, not `session.rs` | ## Files - Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test `crates/loopd/tests/compact_summary.rs`. - Append, from `docs/plans/M5d1/files/runbook-session-compact-failed.md`, to `docs/runbook.md`. - Modify: `crates/loopd/src/compact.rs`, `docs/runbook.md`, `docs/implementer-log.md`. - Do not touch: any other file. `config/compact.md` is already in the tree from task 05. ## Facts you need - **The request.** `ChatRequest { slot: cfg.slots.background, messages, tools: Vec::new(), thinking: true }`. Empty `tools` is omitted from the JSON (`skip_serializing_if` on the body). The given test asserts `body.get("tools").is_none()` and `id_slot == 1` (the test config's background slot). - **The messages.** `messages_with_carry(session.baseline(), session.records(), session.carry())`, then one `ChatMessage::User` whose content is `compact.md` after `trim_end`. Epoch 0 has no carry; later epochs keep the carried summary in the prefix so the model summarises what the session actually sends. - **Read the prompt** with `std::fs::read_to_string(&cfg.compact_prompt())`. An I/O error is `Failed { detail: format!("{}: {e}", path.display()) }` so the path (and `compact.md`) appears in the message. - **The call.** `client.chat_with_retry(&request, &mut |_| {})`. A 400 that is not context-full is not retried (`is_retryable` is already false for it). - **Context-full.** `crate::turn::is_context_full(&e)` then `fallback_summary(session.records(), cfg.compact.summary_chars)`. Do not fall back for a missing file or any other `InferError`. - **The text.** `completion.content.unwrap_or_default()`, then `bxcore::sanitize::neutralise`, then `cap_chars(..., cfg.compact.summary_chars)`. Neutralise first so a tag is not split by the cap. - **`Display`.** `"{detail}; see docs/runbook.md#session-compact-failed"`. The given tests box the error as `dyn Error` and check `to_string()`. - **`compact.rs` stays ≤ 500 lines.** After this task it is about 120. - The items the edits use. This table is complete. | Item | What it does | |---|---| | `Config::compact_prompt` | task 01, `compact.md` beside `system.md` | | `messages_with_carry` | task 03 | | `Client::chat_with_retry` | same as a live turn | | `turn::is_context_full` | the one 400 `exceed_context_size_error` | | `bxcore::sanitize::neutralise` | tags to a visible look-alike | | `fallback_summary`, `cap_chars` | task 05 | | `ChatRequest.tools: Vec::new()` | omits `tools` on the wire | ## The edits, exactly Nothing else in `compact.rs` changes. Each quoted "before" block occurs exactly once after task 05. Keep the fields in the order shown. ### `crates/loopd/src/compact.rs` **E1.** The module doc and the `use` line. The 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}; ``` become ```rust //! Compaction: when a session starts a new epoch, the summary call, and writing the next files //! (M5d spec, section 3). Task 07 writes the next files; task 08 calls this after a turn. use bxcore::sanitize::neutralise; use bxcore::{LogRecord, Usage}; use crate::baseline::messages_with_carry; use crate::config::Config; use crate::llama::{ChatMessage, ChatRequest, Client}; use crate::session::Session; use crate::turn::is_context_full; /// Why a summary call or a next-epoch write failed. Every variant's `Display` ends with the /// runbook pointer `session-compact-failed`. #[derive(Debug)] pub enum CompactError { Failed { detail: String }, } impl std::fmt::Display for CompactError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CompactError::Failed { detail } => write!( f, "{detail}; see docs/runbook.md#session-compact-failed" ), } } } impl std::error::Error for CompactError {} ``` **E2.** After `cap_chars` (the function that ends `s.chars().take(n).collect()` / `}`), add `summarise`. Copy this function whole. There is no `todo!()`. ```rust /// One completion on the background slot: no tools, thinking on, last user `compact.md`. /// Context-full uses `fallback_summary`. A missing prompt, or any other infer error, is /// `CompactError::Failed` with the runbook pointer. No fallback for a missing prompt. pub fn summarise(cfg: &Config, client: &Client, session: &Session) -> Result { let path = cfg.compact_prompt(); let prompt = std::fs::read_to_string(&path).map_err(|e| CompactError::Failed { detail: format!("{}: {e}", path.display()), })?; let mut messages = messages_with_carry(session.baseline(), session.records(), session.carry()); messages.push(ChatMessage::User { content: prompt.trim_end().to_string(), }); let request = ChatRequest { slot: cfg.slots.background, messages, tools: Vec::new(), thinking: true, }; match client.chat_with_retry(&request, &mut |_| {}) { Ok(completion) => { let text = completion.content.unwrap_or_default(); let text = neutralise(&text); Ok(cap_chars(text.as_ref(), cfg.compact.summary_chars)) } Err(e) if is_context_full(&e) => { Ok(fallback_summary(session.records(), cfg.compact.summary_chars)) } Err(e) => Err(CompactError::Failed { detail: e.to_string(), }), } } ``` ## Steps - [ ] **1. Copy the given test.** ```sh cp docs/plans/M5d1/files/crates/loopd/tests/compact_summary.rs crates/loopd/tests/ ``` - [ ] **2. See it fail.** `cargo build --tests --keep-going -p loopd`, timeout 600000 ms: test target `compact_summary` does not compile (`E0432` unresolved import `loopd::compact::CompactError` and/or `E0599` no function `summarise`). Every other target compiles. `compact_due` still compiles. - [ ] **3. The edits.** E1, E2. `cargo check -p loopd` prints no error and no warning. `wc -l crates/loopd/src/compact.rs` prints a number ≤ 500. - [ ] **4. The tests.** `cargo test -p loopd --test compact_summary --test compact_due`: every test passes, 6 in `compact_summary` and 6 in `compact_due`. - [ ] **5. The runbook.** From the repository root: ```sh { echo; cat docs/plans/M5d1/files/runbook-session-compact-failed.md; } >> docs/runbook.md ``` Then `grep -c '^## session-compact-failed$' docs/runbook.md` prints `1`. - [ ] **6. Check.** `git diff --stat` lists `crates/loopd/src/compact.rs` and `docs/runbook.md`. `git status --short` shows `compact_summary.rs` as new. - [ ] **7. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line `gate: ok`. Test count **1495** (1489 before, plus 6). - [ ] **8. Log and commit.** Task cell `M5d1/06-loopd-compact-summary`. Stage with `git add crates/loopd docs/runbook.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when - `make gate` prints `gate: ok` with 1495 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/compact.rs`, `crates/loopd/tests/compact_summary.rs`, `docs/runbook.md` and 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 1495; a comment does not fit its code; `compact.rs` exceeds 500 lines; you add a fallback for a missing `compact.md`; you send tools on the summary request; you use `slots.main` for the summary. ## What the given tests pin The summary request is `id_slot` of the background slot, omits `tools`, has thinking on, and its last user is `compact.md` after `trim_end`; the completion is capped to `summary_chars`; a context-full summary is the epoch's `User` texts; tags in the completion are neutralised; a missing `compact.md` names the file and the runbook; a non-context-full infer error is `CompactError::Failed` with the same pointer.