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

6.7 KiB

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):

    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):

    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

/// 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

/// 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

use crate::baseline::messages;

becomes

use crate::baseline::messages_with_carry;

T2. The lines

        let request = ChatRequest {
            slot: rt.cfg.slots.main,
            messages: messages(baseline, session.records()),
            tools: baseline.tools.clone(),
            thinking: true,
        };

become

        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.

    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.