Files
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

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

  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: SessionFullsession_full; TurnLimitturn_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.