Files
boxmaker/docs/plans/M5d1/06-loopd-compact-summary.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

9.2 KiB

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

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

//! 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!().

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

    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:

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