# M4a task 13: a turn on `loop.sock`, and its answer in the thread **Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop) **Commit subject:** `gatewayd: deliver, turns on loop.sock and answers posted in their thread` ## Goal A batch of messages (task 11) becomes one `turn` on `loopd`'s socket, exactly as `bxctl chat` sends one (read `crates/bxctl/src/chat.rs`, `run_turn`): one frame `{"kind": "turn", "body": {"session", "content", "resume"}}` with id 1, then events until a final `turn_done` or `error`. What comes back is posted in the thread (spec section 8): - No thinking and no status reach Mattermost. The one event that is shown: `approval_pending`, posted once as `waiting for approval : approve or deny it with `bxctl` (Mattermost approvals arrive in M4b)` (the skeleton's `approval_text`). - `turn_done` → its content, split into posts of at most 16,000 **characters** (a Mattermost post holds 16,383), at the last newline before the limit, or at the limit when there is none, in order. An empty answer is posted as `(the answer was empty)`: Mattermost refuses an empty post. - An `error` frame → `Error: : `, where the code is the snake_case name (`no_such_session`), and the detail carries `loopd`'s runbook pointer when it has one. - `loop.sock` cannot be reached, or closes early, or sends a frame that does not belong → `LOOP_DOWN`, and the messages are not sent again: `loopd` may have run them. - A reply in a thread `loopd` does not know (`no_such_session` with `resume: true`) is sent once more with `resume: false`, creating the session, as `bxctl chat --session` does. Only then. ## Files - Copy: `crates/gatewayd/tests/deliver.rs`, `crates/gatewayd/tests/support/fake_loop.rs`, and the skeleton `crates/gatewayd/src/deliver.rs` - Modify: `crates/gatewayd/src/lib.rs` (`pub mod deliver;`), `docs/implementer-log.md` ## The skeleton Written: `MAX_POST` (16,000), `LOOP_DOWN`, `EMPTY_ANSWER`, the trait `Poster` (somewhere to post: Mattermost's `Client`, or a test's record) and its `impl` for `Client`, `Outcome` (`Answer`, `Refused`, `LoopDown(why)`), `approval_text`. To fill: `error_text`, `split_answer`, `one_turn`, `run_turn`, `deliver`. `split_answer` counts **characters**, not bytes, and never cuts inside one: find the byte index of the 16,000th character with `char_indices().nth(MAX_POST)`. ## Steps - [ ] **1. Copy.** `git switch m4a`, then `cp docs/plans/M4a/files/crates/gatewayd/tests/deliver.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/tests/support/fake_loop.rs crates/gatewayd/tests/support/ && cp docs/plans/M4a/files/crates/gatewayd/src/deliver.rs crates/gatewayd/src/`. Add `pub mod deliver;` to `lib.rs`. - [ ] **2. See it fail.** `cargo test -p gatewayd --test deliver`. Expected: it compiles and 9 tests fail. - [ ] **3. Fill `error_text`, `split_answer`, `one_turn`, `run_turn`, `deliver`**, `cargo check -p gatewayd` after each. - [ ] **4. See it pass.** `cargo test -p gatewayd --test deliver`. Expected: 9 passed. - [ ] **5. Run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** `git add crates/gatewayd docs/implementer-log.md Cargo.lock && git commit` ## Done when - `cargo test -p gatewayd --test deliver` passes; `make gate` prints `gate: ok`. ## Stop and report if - A test seems to need a turn sent twice in any case other than the one above.