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

3.6 KiB

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

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.