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>
219 lines
7.9 KiB
Markdown
219 lines
7.9 KiB
Markdown
# 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 test
|
|
`crates/loopd/tests/config_heartbeat.rs` and the starter `config/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_s` is `u64`.** `0` means off. No range check.
|
|
- **`prompt` after `load`.** Join it to the config file's parent, the same way `load` already
|
|
joins `baseline.system`. `heartbeat_prompt()` returns `self.heartbeat.prompt.clone()`. After
|
|
`parse` (no file), the path stays relative.
|
|
- **`check_summarise`.** For each name in `self.compact.summarise`, `registry.get(name)` must be
|
|
`Some`. Empty list is allowed. The error `String` contains both `summarise` and the unknown
|
|
name. `Registry::get` is already `pub`. `http_fetch` and `clock` are in `m5c()`.
|
|
- **`main.rs`.** Both `run_selftest` and `run_serve` load a config. After a successful `load`,
|
|
if `check_summarise` fails, print `loopd: {e}` and return exit 1, as for a load error.
|
|
- **`#[serde(deny_unknown_fields, default)]`** on `Heartbeat`, like `Compact`.
|
|
- The items the edits use. This table is complete.
|
|
|
|
| Item | What it does |
|
|
|---|---|
|
|
| `Registry::get` | known tool names |
|
|
| `Registry::m5c` | what `loopd serve` uses |
|
|
| `Path::join` | prompt 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
|
|
|
|
```rust
|
|
#[serde(default)]
|
|
pub compact: Compact,
|
|
}
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
#[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:
|
|
|
|
```rust
|
|
/// `[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
|
|
|
|
```rust
|
|
if let Some(parent) = path.parent() {
|
|
config.baseline.system = parent.join(&config.baseline.system);
|
|
}
|
|
Ok(config)
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
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)
|
|
|
|
```rust
|
|
pub fn compact_prompt(&self) -> std::path::PathBuf {
|
|
self.baseline.system.with_file_name("compact.md")
|
|
}
|
|
}
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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.**
|
|
|
|
```sh
|
|
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
|
|
target `config_heartbeat` does not compile (`E0609` no field `heartbeat`, `E0599` no method
|
|
`heartbeat_prompt` / `check_summarise`). Every other target compiles.
|
|
- [ ] **3. The edits.** H1 to H4, M1, M2, in order. `cargo check -p loopd` prints no error and
|
|
no warning.
|
|
- [ ] **4. The tests.** `cargo test -p loopd --test config_heartbeat --test config --test
|
|
config_compact`: every test passes, 8 in `config_heartbeat`.
|
|
- [ ] **5. Check.** `git diff --stat` lists `crates/loopd/src/config.rs` and
|
|
`crates/loopd/src/main.rs`. `git status --short` shows the new test and `config/heartbeat.md`.
|
|
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
|
|
`gate: ok`. Test count **1526** (1518 before, plus 8).
|
|
- [ ] **7. Log and commit.** Task cell `M5d2/03-loopd-heartbeat-config`. Stage with
|
|
`git add crates/loopd config/heartbeat.md docs/implementer-log.md Cargo.lock`, then commit
|
|
as `AGENTS.md` says.
|
|
|
|
## Done when
|
|
|
|
- `make gate` prints `gate: ok` with 1526 tests.
|
|
- `git diff HEAD~1 --stat` lists only `crates/loopd/src/config.rs`, `crates/loopd/src/main.rs`,
|
|
`crates/loopd/tests/config_heartbeat.rs`, `config/heartbeat.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 1526; you join `heartbeat.prompt` onto
|
|
`system.md`'s directory instead of the config file's parent; you reject an empty `summarise`
|
|
list; you check names inside `parse`.
|
|
|
|
## 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.
|