# M2b task 07: the channel server **Branch:** `m2b` (run `git switch m2b`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add the channel server on loop.sock` ## Goal Serve turns over `loop.sock` with the M1 frame protocol: one connection, one `turn` frame in, `turn_event` frames out, then one final `turn_done` or `error`. A session runs one turn at a time. ## Files - Copy: `crates/loopd/tests/channel.rs` - Create: `crates/loopd/src/channel.rs` - Modify: `crates/loopd/src/lib.rs`, `docs/implementer-log.md` ## Interfaces ```rust /// Everything a turn needs, shared by every connection. pub struct Context { pub cfg: Config, pub client: Client, pub port: Box, pub registry: Registry, /* private: the set of busy sessions */ } impl Context { pub fn new(cfg: Config, client: Client, port: Box, registry: Registry) -> Context; } /// Accepts connections forever, one thread each. Returns only if `accept` fails. pub fn serve(listener: UnixListener, ctx: Arc) -> std::io::Result<()>; /// One connection. pub fn handle(stream: UnixStream, ctx: Arc); ``` ## What `handle` does 1. `read_frame`. `Closed` before anything: return. Any other error: reply with an `error` frame (`final: true`, id 0) whose code is `BadVersion` for `FrameError::BadVersion`, `BadMessage` for `FrameError::Json`, `BadFrame` otherwise; then return, which closes the connection. 2. The message must be `Message::Turn`; anything else gets `BadMessage`. Every reply frame from here on carries the request's `id` and `v: PROTOCOL_VERSION`. 3. Mark the session busy. If it already is, reply `SessionBusy` and return. Use a `Mutex>` and a guard that removes the id when dropped. 4. `resume: true`: `Session::open`; `false`: `Baseline::assemble` then `Session::create` with `cfg.slots.main`. Then `run_turn` with a `Runtime` built from the context. Each `TurnEvent` is sent as a `turn_event` frame with `final: false`, as it happens. 5. **Release the session before sending the last frame.** A channel that sends its next turn the moment it reads the final frame must not find the session still busy. Drop the guard first, then send `turn_done` (`final: true`) or the error. 6. Errors map to codes: `SessionFull` → `session_full`; `TurnLimit` → `turn_limit`; `Infer(_)` → `inference`; `Session(Exists)` → `session_exists`; `Session(NotFound)` → `no_such_session`; any other `Session` error → `internal`. The detail is the error's `Display` text. 7. A `write_frame` failure means the channel went away: stop sending, finish quietly. ## Steps - [ ] **1. Copy.** `git switch m2b`, then `cp docs/plans/M2b/files/crates/loopd/tests/channel.rs crates/loopd/tests/` - [ ] **2. See the test fail.** `cargo test -p loopd --test channel`. Expected: it does not compile. - [ ] **3. Write `channel.rs`** and add `pub mod channel;` to `lib.rs`. Run `cargo fmt --all`. - [ ] **4. See the tests pass.** `cargo test -p loopd --test channel`, ten times in a row. Expected: `6 passed` every time. If `resume_and_create_are_checked` fails now and then, step 5 of `handle` is not being followed. - [ ] **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 channel` reports 6 passed, ten runs in a row; `make gate` prints `gate: ok`. ## Stop and report if - A test needs the server to keep any session state in memory other than the busy set.