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>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user