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

7.9 KiB

M5d-1 task 01: [compact] on loopd.toml

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] threshold, summary_chars and summarise Log row: the Task cell is M5d1/01-loopd-compact-config.

Goal

M5d-1 compact after a turn whose last Usage is at or above a fraction of expect.n_ctx (spec 3.3, 5). This task adds only the [compact] table so later tasks can read it:

  • threshold (float, default 0.85)
  • summary_chars (default 4000)
  • summarise (list of tool names, default ["http_fetch"])

Nothing in this task uses the values. summarise is stored so a spec-complete loopd.toml loads; M5d-2 reads it. Unknown keys in [compact] are errors, as in every table we define.

Config::compact_prompt is compact.md beside [baseline] system (the same directory join load already does for system.md). The starter file is copied in task 05.

Every place that changes

This is the complete list.

Place What happens there
crates/loopd/src/config.rs Compact; Config.compact; Config::compact_prompt (edits C1 to C3)
crates/loopd/tests/config_compact.rs new, given
everything else no change: not session.rs, not the existing config.rs tests or fixtures

Files

  • Copy, from docs/plans/M5d1/files/ (the same path under it): the new test crates/loopd/tests/config_compact.rs.
  • Modify: crates/loopd/src/config.rs, docs/implementer-log.md.
  • Do not touch: any other file.

Facts you need

  • Config already has #[serde(default)] on optional tables (sampling, limits, paths, channel, loop, baseline, broker, memory). [compact] is the same: missing means Compact::default().
  • threshold is f64. Sampling.temperature is already f64 on Config, so PartialEq on Config still compiles. Do not add a range check: the spec does not name one.
  • summarise default. vec!["http_fetch".to_string()]. An explicit summarise = [] is allowed and is empty. Unknown names are not checked here (M5d-2).
  • compact_prompt. self.baseline.system.with_file_name("compact.md"). After load, baseline.system is already joined to the config file's directory, so the prompt follows it into a subdirectory when system does (prompts/agent.md → prompts/compact.md).
  • The items the edits use. This table is complete; serde is already a dependency of loopd.
    Item What it does
    #[serde(deny_unknown_fields, default)] on Compact: missing table uses Default; unknown keys error
    Path::with_file_name compact.md beside system.md
    Vec<String> summarise

The edits, exactly

Nothing else in this file changes. Each quoted "before" block occurs exactly once in its file when its turn comes (make them in order). Keep the fields in the order shown.

crates/loopd/src/config.rs

C1. After the Memory struct (the lines)

pub struct Memory {
    pub socket: std::path::PathBuf,
}

become

pub struct Memory {
    pub socket: std::path::PathBuf,
}

/// `[compact]`: when a session starts a new epoch, and how long a summary may be (M5d spec,
/// section 5). `summarise` is stored here and unused until M5d-2.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Compact {
    pub threshold: f64,
    pub summary_chars: usize,
    pub summarise: Vec<String>,
}

impl Default for Compact {
    fn default() -> Self {
        Self {
            threshold: 0.85,
            summary_chars: 4_000,
            summarise: vec!["http_fetch".to_string()],
        }
    }
}

C2. The lines

    #[serde(default)]
    pub memory: Memory,
}

become

    #[serde(default)]
    pub memory: Memory,
    #[serde(default)]
    pub compact: Compact,
}

C3. After channel_socket (the lines)

    pub fn channel_socket(&self) -> std::path::PathBuf {
        if self.channel.socket.as_os_str().is_empty() {
            self.paths.home.join("run/loop/loop.sock")
        } else {
            self.channel.socket.clone()
        }
    }
}

become

    pub fn channel_socket(&self) -> std::path::PathBuf {
        if self.channel.socket.as_os_str().is_empty() {
            self.paths.home.join("run/loop/loop.sock")
        } else {
            self.channel.socket.clone()
        }
    }

    /// `compact.md` beside the system prompt: the summary call's last user message.
    pub fn compact_prompt(&self) -> std::path::PathBuf {
        self.baseline.system.with_file_name("compact.md")
    }
}

Steps

  • 1. Copy the given test. From the repository root, one command:

    cp docs/plans/M5d1/files/crates/loopd/tests/config_compact.rs crates/loopd/tests/
    

    Then git status --short lists one new file, crates/loopd/tests/config_compact.rs.

  • 2. See it fail. cargo build --tests --keep-going -p loopd, with a shell timeout of 600000 ms: one test target, config_compact, does not compile, with E0609 (no field \compact` on type `Config`) and E0599 (no method named `compact_prompt`/no variant or associated item named `default` for `Compact``). Every other target compiles.

  • 3. The edits. Make edits C1 to C3, in order. cargo check -p loopd prints no error and no warning.

  • 4. The tests. cargo test -p loopd --test config_compact --test config: every test passes, 8 in config_compact and the others as before.

  • 5. Check. git diff --stat lists exactly crates/loopd/src/config.rs.

  • 6. Run the gate. cargo fmt --all, then make gate, with a shell timeout of 600000 ms. Expected last line: gate: ok. The test count is 1464 after this task, 1456 before it (1455 passed in cargo test --workspace, plus 1), and the 8 new ones are the tests of config_compact.rs. To count, add up the N passed of every test result: line the gate's cargo test --workspace printed, and add 1. Never run the gate again only to count.

  • 7. Log and commit. Add your row to docs/implementer-log.md; its Task cell is M5d1/01-loopd-compact-config. Stage with git add crates/loopd docs/implementer-log.md Cargo.lock, then commit as AGENTS.md says. Cargo.lock does not change in this task; the git add is harmless.

Done when

  • make gate prints gate: ok with 1464 tests.
  • git diff HEAD~1 --stat lists only crates/loopd/src/config.rs, crates/loopd/tests/config_compact.rs and docs/implementer-log.md.

Stop and report if

  • The branch check at the top fails.
  • A "before" block is not in its file exactly as shown when its turn comes, or is there more than once.
  • Step 2 shows a different result: another target failing, or other errors.
  • cargo check in step 3 prints an error or a warning. Quote it, and stop.
  • A test fails in step 4. Do not change the test: stop, and give the failing tests' names and their messages.
  • The fix seems to need a change anywhere else: a fixture, config.rs tests, a range check on threshold. It does not; stop and say why you think so.
  • A test outside config_compact.rs fails in the gate: check once whether it fails without your change (git stash -u, run it, git stash pop), then stop and report both results.
  • The gate passes with a count other than 1464.
  • A comment in an edit does not fit the code under it. Quote it and stop.

What the given tests pin

Each of these changes to the finished code makes at least one given test fail: the default threshold moved; summary_chars default moved; summarise default emptied or given another name; unknown keys accepted; a misspelt summary_char accepted; a partial table wiping the other defaults; compact_prompt using home or the config file's name instead of system.md's directory; Compact::default disagreeing with a missing table.