Add M2b plan: ten tasks, tests, recordings and the first system prompt

The tasks build the agent loop on M2a's client: channel messages and
the usage record in proto, four config tables, the tool port and
registry with find_tool and call_tool, the baseline and replay, the
session store, the turn loop with its limits and the append-only
property test, the channel server, loopd serve, bxctl chat, and the
device checks including a four-turn conversation with a restart.

Checked against a private reference implementation: the gate passes
after every task in order, the new suites pass under CPU load, and the
reference passes make verify-device on straylight with no cache loss.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 17:19:02 -07:00
co-authored by Claude Fable 5.1
parent f238e6a260
commit e156975649
41 changed files with 4883 additions and 3 deletions
+81
View File
@@ -0,0 +1,81 @@
# M2b task 04: the baseline
**Branch:** `m2b` (run `git switch m2b`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add the baseline and the log replay function`
## Goal
The baseline is the fixed prefix of every request in an epoch: the system prompt, `memory/core.md`
if it exists, and the core tool schemas. This task assembles it, writes and reads it as JSON, hashes
it, and turns a session log back into messages.
## Context
From the brief's inference contract: "The request for turn N+1 is a strict extension of the request
for turn N. Nothing volatile goes anywhere but the newest message." The baseline is snapshotted per
session so that an edit to `system.md` changes nothing for a running session. The M0 measurements
show why: any change to an earlier byte re-reads the whole prompt.
## Files
- Copy: `crates/loopd/tests/baseline.rs`, `crates/loopd/tests/support/mod.rs` (replaces the old
one; it gains a `Home` helper and a scripted tool port)
- Create: `crates/loopd/src/baseline.rs`
- Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md`
## Interfaces
```rust
// our format: deny_unknown_fields; derives Debug, Clone, PartialEq, Serialize, Deserialize
pub struct Baseline { pub system: String, pub tools: Vec<ToolSchema> }
pub enum BaselineError { Read(PathBuf, std::io::Error), Parse(PathBuf, serde_json::Error), Hash } // Display names the file; std::error::Error
impl Baseline {
pub fn assemble(cfg: &Config, registry: &Registry) -> Result<Baseline, BaselineError>;
pub fn to_json(&self) -> Result<String, serde_json::Error>;
pub fn from_json(text: &str) -> Result<Baseline, serde_json::Error>;
pub fn load(path: &Path) -> Result<Baseline, BaselineError>;
pub fn hash(&self) -> Result<Hash32, BaselineError>; // proto::sha256 of to_json()
}
/// The message array for a request: the system message, then the log replayed.
pub fn messages(baseline: &Baseline, records: &[LogRecord]) -> Vec<ChatMessage>;
```
Rules:
1. `assemble`: read `cfg.baseline.system`, trim trailing whitespace. If
`<cfg.paths.home>/memory/core.md` is a file whose trimmed content is not empty, append a blank
line and its trimmed content. `tools` is `registry.core_schemas()`.
2. `messages`: `User` gives `ChatMessage::User`; `Assistant` gives `ChatMessage::Assistant` with
its three fields unchanged; `ToolResult` gives `ChatMessage::Tool { tool_call_id, content }`;
`SessionStart`, `Usage`, `CacheLoss` and `EpochEnd` give nothing. Write the match with every
variant named, so that a new variant is a compile error and not a silent skip.
## Steps
- [ ] **1. Copy.**
```sh
git switch m2b
cp docs/plans/M2b/files/crates/loopd/tests/baseline.rs crates/loopd/tests/
cp docs/plans/M2b/files/crates/loopd/tests/support/mod.rs crates/loopd/tests/support/
```
- [ ] **2. See the test fail.** `cargo test -p loopd --test baseline`. Expected: it does not compile.
- [ ] **3. Write `baseline.rs`** and add `pub mod baseline;` to `lib.rs`. Run `cargo fmt --all`.
- [ ] **4. See the tests pass.** `cargo test -p loopd`. Expected: `baseline` 6 passed, and every
earlier file still passes with the new `support/mod.rs`.
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **6. Log and commit.** `git add crates/loopd docs/implementer-log.md && git commit`
## Done when
- `cargo test -p loopd --test baseline` reports 6 passed; `make gate` prints `gate: ok`.
- `cmp crates/loopd/tests/support/mod.rs docs/plans/M2b/files/crates/loopd/tests/support/mod.rs`
prints nothing.
## Stop and report if
- `replay_of_a_prefix_is_a_prefix` fails: `messages` is reordering or rewriting something.