# 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 { 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 { 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 { 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.