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>
This commit is contained in:
2026-10-02 21:54:37 -07:00
co-authored by Grok 4.6
parent 069cd1686a
commit 3726ae585f
48 changed files with 7319 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
# 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<String, CompactError> {
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.