Files
boxmaker/docs/plans/M5d1/03-loopd-carry-messages.md
T
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

186 lines
6.7 KiB
Markdown

# M5d-1 task 03: the carried summary in the next epoch's prefix
**Branch:** `m5d1`. Check it: `git branch --show-current` prints `m5d1` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `loopd: replay a carried summary as an unlogged user message`
**Log row:** the Task cell is `M5d1/03-loopd-carry-messages`.
## Goal
For epoch `n > 0`, the request prefix is the new system message, then one `ChatMessage::User`
whose content is a fixed heading plus the carry, then the new epoch's records (spec 3.5). That
user message is **not** a `LogRecord::User`: it is not a turn, not reported to Honcho, and not
counted by `turns()`. Epoch 0 is unchanged (`carry` is `None`).
`run_turn` must send this prefix, or a compacted session's next live turn would omit the
summary.
## Every place that changes
| Place | What happens there |
|---|---|
| `crates/loopd/src/baseline.rs` | `CARRY_HEADING`; `messages_with_carry`; `messages` calls it with `None` |
| `crates/loopd/src/turn.rs` | the request uses `messages_with_carry` and `session.carry()` |
| `crates/loopd/tests/carry_messages.rs` | new, given |
| everything else | **no change**: not the existing `baseline` tests (they still call `messages`) |
## Files
- Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test
`crates/loopd/tests/carry_messages.rs`.
- Modify: `crates/loopd/src/baseline.rs`, `crates/loopd/src/turn.rs`,
`docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **The heading** is built from pieces so no file holds a chat-template tag whole (T33):
```rust
pub const CARRY_HEADING: &str = concat!("Earlier ", "turns, summarised:");
```
The user content is `format!("{CARRY_HEADING}\n\n{carry}")` when `carry` is `Some`. `None`
inserts nothing.
- **`messages` stays.** Existing tests and `limits.rs` call `messages(baseline, records)`. Make
it `messages_with_carry(baseline, records, None)` so those tests do not change.
- **`run_turn` today** (one place):
```rust
messages: messages(baseline, session.records()),
```
It must pass `session.carry()`. `Session::carry` is `Option<&str>` from task 02.
- Insert the carry user **after** the system message and **before** walking `records`.
- The items the edits use. This table is complete.
| Item | What it does |
|---|---|
| `concat!` | heading from pieces |
| `Session::carry(&self) -> Option<&str>` | task 02 |
| `ChatMessage::User { content }` | the carry, and log users |
## The edits, exactly
### `crates/loopd/src/baseline.rs`
**B1.** After the `Baseline` impl, replace `messages`. The lines
```rust
/// The system message, then each log record replayed in order. Every record variant is named, so a
/// new one is a compile error rather than a silent skip.
pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec<ChatMessage> {
let mut out = vec![ChatMessage::System {
content: baseline.system.clone(),
}];
for record in records {
```
become
```rust
/// The heading in front of a carried summary. Built from pieces (T33).
pub const CARRY_HEADING: &str = concat!("Earlier ", "turns, summarised:");
/// The system message, then each log record replayed in order. Every record variant is named, so a
/// new one is a compile error rather than a silent skip.
pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec<ChatMessage> {
messages_with_carry(baseline, records, None)
}
/// Like `messages`, and when `carry` is `Some` inserts one user message after the system
/// prompt: the heading, a blank line, the summary. That user is not a `LogRecord::User`.
pub fn messages_with_carry(
baseline: &Baseline,
records: &[LogRecord],
carry: Option<&str>,
) -> Vec<ChatMessage> {
let mut out = vec![ChatMessage::System {
content: baseline.system.clone(),
}];
if let Some(summary) = carry {
out.push(ChatMessage::User {
content: format!("{CARRY_HEADING}\n\n{summary}"),
});
}
for record in records {
```
The rest of the `match` is unchanged; do not touch it.
### `crates/loopd/src/turn.rs`
**T1.** The `use` line
```rust
use crate::baseline::messages;
```
becomes
```rust
use crate::baseline::messages_with_carry;
```
**T2.** The lines
```rust
let request = ChatRequest {
slot: rt.cfg.slots.main,
messages: messages(baseline, session.records()),
tools: baseline.tools.clone(),
thinking: true,
};
```
become
```rust
let request = ChatRequest {
slot: rt.cfg.slots.main,
messages: messages_with_carry(baseline, session.records(), session.carry()),
tools: baseline.tools.clone(),
thinking: true,
};
```
## Steps
- [ ] **1. Copy the given test.**
```sh
cp docs/plans/M5d1/files/crates/loopd/tests/carry_messages.rs crates/loopd/tests/
```
- [ ] **2. See it fail.** `cargo build --tests --keep-going -p loopd`, timeout 600000 ms: test
target `carry_messages` does not compile (`E0432` unresolved import
`loopd::baseline::{CARRY_HEADING, messages_with_carry}`). Every other target compiles.
- [ ] **3. The edits.** B1, T1, T2. `cargo check -p loopd` prints no error and no warning.
- [ ] **4. The tests.** `cargo test -p loopd --test carry_messages --test baseline --test turn`:
every test passes, 5 in `carry_messages` and the others as before.
- [ ] **5. Check.** `git diff --stat` lists exactly `crates/loopd/src/baseline.rs` and
`crates/loopd/src/turn.rs`.
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
`gate: ok`. Test count **1478** (1473 before, plus 5).
- [ ] **7. Log and commit.** Task cell `M5d1/03-loopd-carry-messages`. Stage with
`git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says.
## Done when
- `make gate` prints `gate: ok` with 1478 tests.
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/baseline.rs`, `crates/loopd/src/turn.rs`,
`crates/loopd/tests/carry_messages.rs` and `docs/implementer-log.md`.
## 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 1478; a comment does not fit its code.
- You think `messages` should insert the carry. It must not: existing tests call `messages` and
would see an extra user. `run_turn` is the one call that must pass `session.carry()`.
## What the given tests pin
Epoch 0 with `None` matches `messages`; a carry is a user after the system prompt and before the
epoch's users; the carry is not in the log records; the heading is the `concat!` pieces; the
first request of epoch 1 is not a prefix-extension of epoch 0's messages.