Files
boxmaker/docs/plans/M2b/07-loopd-channel.md
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

71 lines
3.5 KiB
Markdown

# 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<dyn ToolPort>, pub registry: Registry, /* private: the set of busy sessions */ }
impl Context { pub fn new(cfg: Config, client: Client, port: Box<dyn ToolPort>, registry: Registry) -> Context; }
/// Accepts connections forever, one thread each. Returns only if `accept` fails.
pub fn serve(listener: UnixListener, ctx: Arc<Context>) -> std::io::Result<()>;
/// One connection.
pub fn handle(stream: UnixStream, ctx: Arc<Context>);
```
## 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<HashSet<SessionId>>` 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.