Files
boxmaker/docs/plans/M2b/05-loopd-session.md
T
kyleandClaude Fable 5.1 e156975649 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>
2026-09-18 17:19:02 -07:00

80 lines
3.6 KiB
Markdown

# M2b task 05: sessions on disk
**Branch:** `m2b` (run `git switch m2b`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Add the session store: baseline file and append-only log`
## Goal
A session is a directory: `sessions/<id>/0.baseline.json` and `sessions/<id>/0.jsonl`. This task
creates, opens, appends and resumes. The log is the state; there is no other file.
## Context
From the spec: "Nothing is written for a request until its completion is final, so the log always
ends at a record boundary." A log that does not is refused at resume, with the file and line named.
Nothing is repaired silently. The same rule as the audit log in the pre-M1 spec.
## Files
- Copy: `crates/loopd/tests/session.rs`
- Create: `crates/loopd/src/session.rs`
- Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md`
## Interfaces
```rust
pub enum SessionError {
Exists(SessionId),
NotFound(SessionId),
Io(PathBuf, std::io::Error),
Torn { path: PathBuf, line: usize, why: String }, // lines start at 1; Display shows "<path>:<line>: …"
Baseline(BaselineError),
Encode(serde_json::Error),
} // Debug; Display; std::error::Error
pub struct Session { /* private: id, dir, baseline, records, the open log file, next_call */ }
impl Session {
pub fn dir_for(home: &Path, id: &SessionId) -> PathBuf; // <home>/sessions/<id>
pub fn create(home: &Path, id: SessionId, baseline: Baseline, slot: u32) -> Result<Session, SessionError>;
pub fn open(home: &Path, id: SessionId) -> Result<Session, SessionError>;
pub fn id(&self) -> &SessionId;
pub fn dir(&self) -> &Path;
pub fn baseline(&self) -> &Baseline;
pub fn records(&self) -> &[LogRecord];
pub fn append(&mut self, record: LogRecord) -> Result<(), SessionError>;
pub fn next_call(&mut self) -> CallId; // 1, 2, 3, … within a session
pub fn last_usage(&self) -> Option<proto::Usage>; // the latest Usage record's numbers
}
```
Rules:
1. `create`: the directory must not exist (`Exists`). Create it, write `0.baseline.json` from
`to_json()`, open `0.jsonl` with `create_new` and append, then append a `SessionStart` record
with `Timestamp::now()`, epoch 0, the slot, and `baseline.hash()`.
2. `open`: `0.jsonl` must exist (`NotFound`). Read the baseline from `0.baseline.json`. Read the
log: every line must end in `\n` and parse as a `LogRecord`, else `Torn` with its 1-based
line number and the reason. `next_call` becomes one more than the highest `call` of any
`ToolResult` in the log, or 1.
3. `append`: encode the record as one line, write it, `sync_data()`, and only then push it to the
in-memory list. A record is never in memory without being on disk.
4. No `unwrap`. Every I/O error carries the path it concerns.
## Steps
- [ ] **1. Copy.** `git switch m2b`, then
`cp docs/plans/M2b/files/crates/loopd/tests/session.rs crates/loopd/tests/`
- [ ] **2. See the test fail.** `cargo test -p loopd --test session`. Expected: it does not compile.
- [ ] **3. Write `session.rs`** and add `pub mod session;` to `lib.rs`. Run `cargo fmt --all`.
- [ ] **4. See the tests pass.** `cargo test -p loopd --test session`. Expected: `7 passed`.
- [ ] **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 session` reports 7 passed; `make gate` prints `gate: ok`.
## Stop and report if
- `a_torn_log_is_refused_with_the_line_number` cannot pass without editing the log on disk.