Files
boxmaker/docs/plans/M5d1/04-memoryd-honcho-epochs.md
T
kyleandGrok 4.6 c3b05cf003 docs: M5d-1 checklist fixes before hand-over
Given tests: Action::Turn (not To), send_turn of epoch 1, clippy on
compact_summary, jsonl-without-baseline, live turn after compact sends
the carry. send_turn maps Damaged to Local("log") as read_turn did.
Gate counts 1484 → 1508. Mutations recorded in the README.

Co-Authored-By: Grok 4.6 <noreply@x.ai>
2026-10-02 22:36:51 -07:00

248 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M5d-1 task 04: Honcho reads every epoch file
**Branch:** `m5d1`. Check it: `git branch --show-current` prints `m5d1` and `git status --short`
is empty; otherwise stop. Never switch branch.
**Commit subject:** `memoryd: Honcho concatenates every epoch of a session`
**Log row:** the Task cell is `M5d1/04-memoryd-honcho-epochs`.
## Goal
`memoryd` keys a turn as `Item::Turn { session, turn }` with no epoch (spec 3.2). After
compaction, turn 3 may live in `1.jsonl`. `log_path` still names `0.jsonl` (existing tests pin
that path). A new `read_session` concatenates every `<n>.jsonl` in epoch order. `TurnSync`
walks that concatenation, and the worker's `send_turn` does too, so a turn in epoch 1 is
decided and sent.
`turn_records` / `turns` do not change: they already count `User` records on a slice.
## Every place that changes
| Place | What happens there |
|---|---|
| `crates/memoryd/src/honcho/turns.rs` | `epoch_logs`; `read_session`; `records` and `resume` use it |
| `crates/memoryd/src/honcho/worker.rs` | `send_turn` reads the concatenated log (W1–W3) |
| `crates/memoryd/tests/honcho_epochs.rs` | new, given |
| everything else | **no change**: not `log_path`'s return value, not `decide.rs` |
## Files
- Copy, from `docs/plans/M5d1/files/` (the same path under it): the new test
`crates/memoryd/tests/honcho_epochs.rs`.
- Modify: `crates/memoryd/src/honcho/turns.rs`, `crates/memoryd/src/honcho/worker.rs`,
`docs/implementer-log.md`.
- Do not touch: any other file.
## Facts you need
- **`read_records` / `LogError`** already exist in `turns.rs`. `read_session` returns
`Result<Vec<LogRecord>, LogError>` so a damaged line still names its file and
`LOG_RUNBOOK`.
- **Listing.** Under `sessions/<id>/`, every file named `<n>.jsonl` where `n` parses as `u32`,
sorted by `n`. A missing session directory is empty, not an error (same as a missing
`0.jsonl` today).
- **`log_path` stays** `.../0.jsonl`. Do not change its body. `readable` in the worker still
names that path when the concatenated read succeeds, so existing recovery log lines stay
stable.
- **`send_turn` today** calls `read_turn(&log_path(...), turn)`, which only sees epoch 0. After
this task it loads `read_session`, then `turn_records` / `user_text` / `final_reply` from
`crate::honcho::decide` (already `pub`). `read_turn` maps a damaged line to `Ok(None)` (the
turn is marked failed, kind `log`). Keep that: `LogError::Damaged` is `Unsent::Local("log")`;
an I/O error stays `Unsent::Unreadable` and is retried.
- `turns.rs` after this task stays under 500 lines (it is 332 now).
- The items the edits use. This table is complete.
| Item | What it does |
|---|---|
| `read_records(&Path) -> Result<Vec<LogRecord>, LogError>` | one file, already in `turns.rs` |
| `turn_records(&[LogRecord], u64) -> Option<&[LogRecord]>` | `decide.rs` |
| `user_text(&[LogRecord]) -> (&str, Option<&str>)` | `decide.rs` |
| `final_reply(&[LogRecord]) -> Option<&str>` | `decide.rs` |
| `str::strip_suffix(".jsonl")`, `str::parse::<u32>` | epoch numbers |
| `Unsent::Unreadable(String)` | worker, `e.to_string()` |
## The edits, exactly
### `crates/memoryd/src/honcho/turns.rs`
**H1.** The doc comment on `log_path`, the lines
```rust
/// The path of `session`'s log under `sessions`: epoch 0, the only one `loopd` writes.
pub fn log_path(sessions: &Path, session: &SessionId) -> PathBuf {
```
become
```rust
/// The path of `session`'s epoch-0 log under `sessions`. Later epochs are sibling files;
/// `read_session` concatenates them. Existing tests pin this path.
pub fn log_path(sessions: &Path, session: &SessionId) -> PathBuf {
```
**H1b.** After `read_records` (the function that ends `Ok(records)` / `}`), insert these two
functions. They sit below `LogError` and `read_records`, so both names resolve.
```rust
/// Every `<n>.jsonl` of `session`, sorted by `n`. A missing directory is empty.
pub fn epoch_logs(sessions: &Path, session: &SessionId) -> Result<Vec<PathBuf>, LogError> {
let dir = sessions.join(session.as_str());
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(LogError::Io(format!("{}: {e}", dir.display()))),
};
let mut found: Vec<(u32, PathBuf)> = Vec::new();
for entry in entries {
let entry = match entry {
Ok(e) => e,
Err(e) => return Err(LogError::Io(format!("{}: {e}", dir.display()))),
};
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
let Some(stem) = name.strip_suffix(".jsonl") else { continue };
let Ok(n) = stem.parse::<u32>() else { continue };
found.push((n, entry.path()));
}
found.sort_by_key(|(n, _)| *n);
Ok(found.into_iter().map(|(_, p)| p).collect())
}
/// Every epoch's records, in epoch order. A missing session has none.
pub fn read_session(
sessions: &Path,
session: &SessionId,
) -> Result<Vec<LogRecord>, LogError> {
let mut out = Vec::new();
for path in epoch_logs(sessions, session)? {
out.extend(read_records(&path)?);
}
Ok(out)
}
```
The next function after this insert is `finished`. Do not change `finished`.
**H2.** In `resume`, the line
```rust
if let Ok(records) = read_log(&log_path(&self.sessions, &session)) {
```
becomes
```rust
if let Ok(records) = read_session(&self.sessions, &session) {
```
**H3.** In `records`, the line
```rust
match read_log(&log_path(&self.sessions, session)) {
```
becomes
```rust
match read_session(&self.sessions, session).map_err(|e| e.to_string()) {
```
`read_log` already mapped `LogError` through `Display`; this keeps the logged line the same
shape (`memoryd: {why}; {LOG_RUNBOOK}`).
### `crates/memoryd/src/honcho/worker.rs`
**W1.** The `use` line
```rust
use crate::honcho::turns::{TurnSync, log_path};
```
becomes
```rust
use crate::honcho::decide::{final_reply, turn_records, user_text};
use crate::honcho::turns::{LogError, TurnSync, log_path, read_session};
```
**W2.** The body of `send_turn`, the lines
```rust
let path = log_path(&self.sessions, session);
let read = read_turn(&path, turn).map_err(Unsent::Unreadable)?;
self.readable(&path);
let (text, reply) = read.ok_or(Unsent::Local("log"))?;
```
become
```rust
let path = log_path(&self.sessions, session);
let records = match read_session(&self.sessions, session) {
Ok(records) => {
self.readable(&path);
records
}
Err(LogError::Damaged(_)) => return Err(Unsent::Local("log")),
Err(e) => return Err(Unsent::Unreadable(e.to_string())),
};
let slice = turn_records(&records, turn).ok_or(Unsent::Local("log"))?;
let (text, _) = user_text(slice);
let reply = final_reply(slice).ok_or(Unsent::Local("log"))?;
```
**W3.** The `batch` call, the line
```rust
let messages = batch(&text, &reply, speaker, &self.agent, session, turn);
```
becomes
```rust
let messages = batch(text, reply, speaker, &self.agent, session, turn);
```
`text` and `reply` are already `&str`; clippy denies the extra borrows (`needless_borrow`).
The rest of `send_turn` is unchanged (`honcho.ok_or`, `MAX_BATCH`, `add_messages`).
## Steps
- [ ] **1. Copy the given test.**
```sh
cp docs/plans/M5d1/files/crates/memoryd/tests/honcho_epochs.rs crates/memoryd/tests/
```
- [ ] **2. See it fail.** `cargo build --tests --keep-going -p memoryd`, timeout 600000 ms: test
target `honcho_epochs` does not compile (`E0432` unresolved import
`memoryd::honcho::turns::read_session`). Every other target compiles.
- [ ] **3. The edits.** H1, H1b, H2, H3, W1, W2, W3. `cargo check -p memoryd`
prints no error and no warning. `wc -l crates/memoryd/src/honcho/turns.rs` and
`wc -l crates/memoryd/src/honcho/worker.rs` are each ≤ 500.
- [ ] **4. The tests.** `cargo test -p memoryd --test honcho_epochs --test honcho_turns --test
honcho_worker`: every test passes, 6 in `honcho_epochs` and the others as before.
- [ ] **5. Check.** `git diff --stat` lists exactly `crates/memoryd/src/honcho/turns.rs` and
`crates/memoryd/src/honcho/worker.rs`.
- [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line
`gate: ok`. Test count **1484** (1478 before, plus 6).
- [ ] **7. Log and commit.** Task cell `M5d1/04-memoryd-honcho-epochs`. Stage with
`git add crates/memoryd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says.
## Done when
- `make gate` prints `gate: ok` with 1484 tests.
- `git diff HEAD~1 --stat` lists only the two `honcho` sources, `honcho_epochs.rs`, and the log.
## Stop and report if
- The branch check fails; a "before" block is not found exactly once; step 2 differs; a file
exceeds 500 lines; a test fails; the gate count is not 1484.
- You think `log_path` should return every file. It must still return `0.jsonl`.
## What the given tests pin
Two users in epoch 0 and one in epoch 1 is `turns() == 3` and `turn_records(..., 3)` is the
epoch-1 user; epoch 0 alone still reads; a damaged `1.jsonl` names that file; `TurnSync` decides
turn 3 from epoch 1; `send_turn` posts turn 3 from epoch 1 (three worker steps, oldest first);
a missing session directory is empty.