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>
3.6 KiB
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:
create: the directory must not exist (Exists). Create it, write0.baseline.jsonfromto_json(), open0.jsonlwithcreate_newand append, then append aSessionStartrecord withTimestamp::now(), epoch 0, the slot, andbaseline.hash().open:0.jsonlmust exist (NotFound). Read the baseline from0.baseline.json. Read the log: every line must end in\nand parse as aLogRecord, elseTornwith its 1-based line number and the reason.next_callbecomes one more than the highestcallof anyToolResultin the log, or 1.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.- No
unwrap. Every I/O error carries the path it concerns.
Steps
- 1. Copy.
git switch m2b, thencp 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.rsand addpub mod session;tolib.rs. Runcargo 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 sessionreports 7 passed;make gateprintsgate: ok.
Stop and report if
a_torn_log_is_refused_with_the_line_numbercannot pass without editing the log on disk.