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.5 KiB
3.5 KiB
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
/// 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
read_frame.Closedbefore anything: return. Any other error: reply with anerrorframe (final: true, id 0) whose code isBadVersionforFrameError::BadVersion,BadMessageforFrameError::Json,BadFrameotherwise; then return, which closes the connection.- The message must be
Message::Turn; anything else getsBadMessage. Every reply frame from here on carries the request'sidandv: PROTOCOL_VERSION. - Mark the session busy. If it already is, reply
SessionBusyand return. Use aMutex<HashSet<SessionId>>and a guard that removes the id when dropped. resume: true:Session::open;false:Baseline::assemblethenSession::createwithcfg.slots.main. Thenrun_turnwith aRuntimebuilt from the context. EachTurnEventis sent as aturn_eventframe withfinal: false, as it happens.- 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. - Errors map to codes:
SessionFull→session_full;TurnLimit→turn_limit;Infer(_)→inference;Session(Exists)→session_exists;Session(NotFound)→no_such_session; any otherSessionerror →internal. The detail is the error'sDisplaytext. - A
write_framefailure means the channel went away: stop sending, finish quietly.
Steps
- 1. Copy.
git switch m2b, thencp 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.rsand addpub mod channel;tolib.rs. Runcargo fmt --all. - 4. See the tests pass.
cargo test -p loopd --test channel, ten times in a row. Expected:6 passedevery time. Ifresume_and_create_are_checkedfails now and then, step 5 ofhandleis 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 channelreports 6 passed, ten runs in a row;make gateprintsgate: ok.
Stop and report if
- A test needs the server to keep any session state in memory other than the busy set.