# M2b task 04: the baseline **Branch:** `m2b` (run `git switch m2b`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add the baseline and the log replay function` ## Goal The baseline is the fixed prefix of every request in an epoch: the system prompt, `memory/core.md` if it exists, and the core tool schemas. This task assembles it, writes and reads it as JSON, hashes it, and turns a session log back into messages. ## Context From the brief's inference contract: "The request for turn N+1 is a strict extension of the request for turn N. Nothing volatile goes anywhere but the newest message." The baseline is snapshotted per session so that an edit to `system.md` changes nothing for a running session. The M0 measurements show why: any change to an earlier byte re-reads the whole prompt. ## Files - Copy: `crates/loopd/tests/baseline.rs`, `crates/loopd/tests/support/mod.rs` (replaces the old one; it gains a `Home` helper and a scripted tool port) - Create: `crates/loopd/src/baseline.rs` - Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md` ## Interfaces ```rust // our format: deny_unknown_fields; derives Debug, Clone, PartialEq, Serialize, Deserialize pub struct Baseline { pub system: String, pub tools: Vec } pub enum BaselineError { Read(PathBuf, std::io::Error), Parse(PathBuf, serde_json::Error), Hash } // Display names the file; std::error::Error impl Baseline { pub fn assemble(cfg: &Config, registry: &Registry) -> Result; pub fn to_json(&self) -> Result; pub fn from_json(text: &str) -> Result; pub fn load(path: &Path) -> Result; pub fn hash(&self) -> Result; // proto::sha256 of to_json() } /// The message array for a request: the system message, then the log replayed. pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec; ``` Rules: 1. `assemble`: read `cfg.baseline.system`, trim trailing whitespace. If `/memory/core.md` is a file whose trimmed content is not empty, append a blank line and its trimmed content. `tools` is `registry.core_schemas()`. 2. `messages`: `User` gives `ChatMessage::User`; `Assistant` gives `ChatMessage::Assistant` with its three fields unchanged; `ToolResult` gives `ChatMessage::Tool { tool_call_id, content }`; `SessionStart`, `Usage`, `CacheLoss` and `EpochEnd` give nothing. Write the match with every variant named, so that a new variant is a compile error and not a silent skip. ## Steps - [ ] **1. Copy.** ```sh git switch m2b cp docs/plans/M2b/files/crates/loopd/tests/baseline.rs crates/loopd/tests/ cp docs/plans/M2b/files/crates/loopd/tests/support/mod.rs crates/loopd/tests/support/ ``` - [ ] **2. See the test fail.** `cargo test -p loopd --test baseline`. Expected: it does not compile. - [ ] **3. Write `baseline.rs`** and add `pub mod baseline;` to `lib.rs`. Run `cargo fmt --all`. - [ ] **4. See the tests pass.** `cargo test -p loopd`. Expected: `baseline` 6 passed, and every earlier file still passes with the new `support/mod.rs`. - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** `git add crates/loopd docs/implementer-log.md && git commit` ## Done when - `cargo test -p loopd --test baseline` reports 6 passed; `make gate` prints `gate: ok`. - `cmp crates/loopd/tests/support/mod.rs docs/plans/M2b/files/crates/loopd/tests/support/mod.rs` prints nothing. ## Stop and report if - `replay_of_a_prefix_is_a_prefix` fails: `messages` is reordering or rewriting something.