The M5d-1 gate on straylight is 1508, so every M5d-2 expected count moves up by one. OpenCode 1.15.10 serves Ornith as llama.cpp/ornith-1.5-35b-a3b. Co-Authored-By: Grok 4.6 <noreply@x.ai>
7.9 KiB
M5d-2 task 03: [heartbeat] and unknown [compact] summarise names
Branch: m5d2. Check it: git branch --show-current prints m5d2 and git status --short
is empty; otherwise stop. Never switch branch.
Commit subject: loopd: [heartbeat] table and summarise name check
Log row: the Task cell is M5d2/03-loopd-heartbeat-config.
Goal
[heartbeat] on loopd.toml (spec 4.1, 5): every_s (default 0, disables) and prompt
(default heartbeat.md, next to the config file). Unknown keys are errors.
Unknown names in [compact] summarise are a config error (spec 4.3). Config::check_summarise
takes &Registry. loopd calls it after load with Registry::m5c(). parse still succeeds
with an unknown name: the given tests call check_summarise themselves.
The starter config/heartbeat.md is copied like config/system.md. Nothing ticks yet.
Every place that changes
| Place | What happens there |
|---|---|
crates/loopd/src/config.rs |
Heartbeat; field; load joins the prompt; heartbeat_prompt; check_summarise |
crates/loopd/src/main.rs |
after each Config::load, check_summarise(&Registry::m5c()) |
config/heartbeat.md |
new, given starter |
crates/loopd/tests/config_heartbeat.rs |
new, given |
| everything else | no change |
Files
- Copy, from
docs/plans/M5d2/files/(the same path under it): the new testcrates/loopd/tests/config_heartbeat.rsand the starterconfig/heartbeat.md. - Modify:
crates/loopd/src/config.rs,crates/loopd/src/main.rs,docs/implementer-log.md. - Do not touch: any other file.
Facts you need
every_sisu64.0means off. No range check.promptafterload. Join it to the config file's parent, the same wayloadalready joinsbaseline.system.heartbeat_prompt()returnsself.heartbeat.prompt.clone(). Afterparse(no file), the path stays relative.check_summarise. For each name inself.compact.summarise,registry.get(name)must beSome. Empty list is allowed. The errorStringcontains bothsummariseand the unknown name.Registry::getis alreadypub.http_fetchandclockare inm5c().main.rs. Bothrun_selftestandrun_serveload a config. After a successfulload, ifcheck_summarisefails, printloopd: {e}and return exit 1, as for a load error.#[serde(deny_unknown_fields, default)]onHeartbeat, likeCompact.- The items the edits use. This table is complete.
Item What it does Registry::getknown tool names Registry::m5cwhat loopd serveusesPath::joinprompt beside the config file
The edits, exactly
Each quoted "before" block occurs exactly once after M5d-1 (compact is already on Config).
crates/loopd/src/config.rs
H1. After the Compact Default impl (the closing } of that impl, then the Config
struct's memory / compact fields). After
#[serde(default)]
pub compact: Compact,
}
become
#[serde(default)]
pub compact: Compact,
#[serde(default)]
pub heartbeat: Heartbeat,
}
H2. After the Compact Default impl, add Heartbeat (before the Config struct if that
is where Compact sits; keep Heartbeat next to Compact). The new type:
/// `[heartbeat]`: how often the standing `heartbeat` session runs (M5d spec, section 4.1).
/// `every_s` 0 disables it.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Heartbeat {
pub every_s: u64,
pub prompt: std::path::PathBuf,
}
impl Default for Heartbeat {
fn default() -> Self {
Self {
every_s: 0,
prompt: std::path::PathBuf::from("heartbeat.md"),
}
}
}
If Compact sits just above Config, put Heartbeat immediately after Compact's Default
impl and then do H1. If a "before" block is not found exactly once, stop.
H3. In load, the lines
if let Some(parent) = path.parent() {
config.baseline.system = parent.join(&config.baseline.system);
}
Ok(config)
become
if let Some(parent) = path.parent() {
config.baseline.system = parent.join(&config.baseline.system);
config.heartbeat.prompt = parent.join(&config.heartbeat.prompt);
}
Ok(config)
H4. After compact_prompt (the lines)
pub fn compact_prompt(&self) -> std::path::PathBuf {
self.baseline.system.with_file_name("compact.md")
}
}
become
pub fn compact_prompt(&self) -> std::path::PathBuf {
self.baseline.system.with_file_name("compact.md")
}
/// The heartbeat prompt, already joined to the config file's directory by `load`.
pub fn heartbeat_prompt(&self) -> std::path::PathBuf {
self.heartbeat.prompt.clone()
}
/// Unknown `[compact] summarise` names are a config error (M5d spec, section 4.3).
pub fn check_summarise(&self, registry: &crate::tools::Registry) -> Result<(), String> {
for name in &self.compact.summarise {
if registry.get(name).is_none() {
return Err(format!("[compact] summarise: unknown tool {name}"));
}
}
Ok(())
}
}
crates/loopd/src/main.rs
M1. In run_selftest, after a successful Config::load and before Client::new, add:
if let Err(e) = cfg.check_summarise(&loopd::tools::Registry::m5c()) {
eprintln!("loopd: {e}");
return ExitCode::from(1);
}
Registry is already imported in main.rs. Use Registry::m5c().
M2. The same check in run_serve, after a successful load and before the instance lock.
If main.rs already has use loopd::tools::{Registry, ToolPort};, write Registry::m5c().
Steps
-
1. Copy the given files.
cp docs/plans/M5d2/files/crates/loopd/tests/config_heartbeat.rs crates/loopd/tests/ cp docs/plans/M5d2/files/config/heartbeat.md config/heartbeat.md -
2. See it fail.
cargo build --tests --keep-going -p loopd, timeout 600000 ms: test targetconfig_heartbeatdoes not compile (E0609no fieldheartbeat,E0599no methodheartbeat_prompt/check_summarise). Every other target compiles. -
3. The edits. H1 to H4, M1, M2, in order.
cargo check -p loopdprints no error and no warning. -
4. The tests.
cargo test -p loopd --test config_heartbeat --test config --test config_compact: every test passes, 8 inconfig_heartbeat. -
5. Check.
git diff --statlistscrates/loopd/src/config.rsandcrates/loopd/src/main.rs.git status --shortshows the new test andconfig/heartbeat.md. -
6. Run the gate.
cargo fmt --all, thenmake gate, timeout 600000 ms. Last linegate: ok. Test count 1526 (1518 before, plus 8). -
7. Log and commit. Task cell
M5d2/03-loopd-heartbeat-config. Stage withgit add crates/loopd config/heartbeat.md docs/implementer-log.md Cargo.lock, then commit asAGENTS.mdsays.
Done when
make gateprintsgate: okwith 1526 tests.git diff HEAD~1 --statlists onlycrates/loopd/src/config.rs,crates/loopd/src/main.rs,crates/loopd/tests/config_heartbeat.rs,config/heartbeat.mdand the log.
Stop and report if
- The branch check fails; a "before" block is not found exactly once; step 2 differs;
cargo checkwarns; a test fails; the gate count is not 1526; you joinheartbeat.promptontosystem.md's directory instead of the config file's parent; you reject an emptysummariselist; you check names insideparse.
What the given tests pin
A missing table is disabled and heartbeat_prompt is beside the config file; overrides replace
defaults; unknown keys error; default http_fetch is known to m5c; an unknown name errors
with summarise and that name; empty is allowed; http_fetch and clock together are known.