Files
boxmaker/docs/plans/M4a/11-gatewayd-sessions.md
T
kyleandClaude Opus 5.5 0339dc13b2 Plan M4a: gatewayd in 15 tasks, with skeletons and given tests
Each task's tests were run against a reference at its end state; the end states were replayed
from master in order with the gate at each step (650 to 762 tests); each skeleton compiles
against its tests and fails them. The reference is kept off this machine. Lessons T27 (every
wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:05:44 -07:00

81 lines
4.4 KiB
Markdown

# M4a task 11: which posts become turns, and the queue per session
**Branch:** `m4a` (run `git switch m4a`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `gatewayd: sessions, routing posts to sessions, commands and the queue`
## Goal
The decisions, as pure code: no network, no files. For each new post, **in this order** (spec
section 7):
1. Ignore it if it is the bot's own, or a system message (`kind` not empty).
2. Ignore it, **silently**, unless the author is in `allow.users`.
3. Is it for this Boxmaker? A direct message (`"D"`): always. A channel or group message (`"O"`,
`"P"`, `"G"`) whose id is in `allow.channels`: if it **names this bot**, or if it is a reply in a
thread this Boxmaker already has a session for **and names nobody else**. Anything else: ignore.
`@channel`, `@here` and `@all` name nobody: every agent would answer them.
4. The session: its root is `root_id`, or the post's own id when that is empty; the session id is
`mm-<root>`. A top-level post is a new session (`resume: false`); a reply resumes it.
5. Commands: a message starting with `!`. `!!…` is not a command: one `!` is removed and the rest
goes on as a message. `!approve …` and `!deny …` are answered with `M4B_COMMAND`; any other `!`
with `UNKNOWN_COMMAND`. A command never reaches `loopd`.
6. Otherwise the message joins its session's queue.
"Names" means `@<username>` in the message, case-insensitive, where the name is the longest run of
`a-z`, `0-9`, `.`, `-` and `_` after the `@`, without trailing dots. The spec's examples, with this
bot called `boxmaker-straylight` and another agent called Hermes in the channel (the first test
checks every row):
| Post | For Boxmaker? |
|---|---|
| `@boxmaker-straylight summarise the audit log` (top level) | yes: a new session rooted here |
| a reply in that thread: `and the older files?` | yes: its thread, nobody else named |
| a reply in that thread: `@hermes what do you think?` | no |
| `@boxmaker-straylight @hermes compare notes` | yes (and Hermes answers too) |
| `@boxmaker-straylightx hello` | no: a different name |
| `@channel standup in five` | no |
**The queue.** One turn at a time per session. A message for a session with no turn running
starts one at once. Messages that arrive while it runs wait; when it ends, **all** waiting messages
go together as the next turn, joined with a blank line (`"\n\n"`), in the order they came. A
session with `limit` messages already waiting drops the next one, and the caller answers `BUSY`.
## Files
- Copy: `crates/gatewayd/tests/sessions.rs`, and the skeleton `crates/gatewayd/src/sessions.rs`
- Modify: `crates/gatewayd/src/lib.rs` (`pub mod sessions;`), `docs/implementer-log.md`
## The skeleton
Written: the three answer texts (`M4B_COMMAND`, `UNKNOWN_COMMAND`, `BUSY`), `EVERYONE`, `Ignored`
(`Own`, `System`, `NotAllowed`, `NotForUs`), `Thread { channel, root }`, `Message { session,
thread, resume, text, joins_thread }`, `Route` (`Ignore`, `Reply { thread, text }`, `Queue`),
`Router`, `Batch { session, thread, resume, text }`, `Pushed` (`Start(Batch)`, `Waiting`,
`Full(Thread)`), `Pending`, `Queues`.
To fill: `named`, `Router::new`, `Router::for_us`, `Router::route`, `Queues::new`, `push`,
`finish`, `running`, `threads`. `route` is a straight line of early returns in the order above; the
`known` function it is given answers "does this Boxmaker have a session for this thread root?"
(task 14 answers it from the state file). `SessionId` has no `Ord`, so `Queues` keys a `HashMap`.
## Steps
- [ ] **1. Copy.** `git switch m4a`, then
`cp docs/plans/M4a/files/crates/gatewayd/tests/sessions.rs crates/gatewayd/tests/ && cp docs/plans/M4a/files/crates/gatewayd/src/sessions.rs crates/gatewayd/src/`.
Add `pub mod sessions;` to `lib.rs`.
- [ ] **2. See it fail.** `cargo test -p gatewayd --test sessions`. Expected: it compiles and 8
tests fail.
- [ ] **3. Fill `named`, then the `Router`, then the `Queues`**, `cargo check -p gatewayd` after
each function.
- [ ] **4. See it pass.** `cargo test -p gatewayd --test sessions`. Expected: 8 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 sessions` passes; `make gate` prints `gate: ok`.
## Stop and report if
- A row of the table above seems to need a different order of the steps.