gatewayd: approvals asked by non-approvers in direct messages go to an approver

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-24 12:07:06 -07:00
parent 989548419d
commit e10d93c3e8
6 changed files with 338 additions and 42 deletions
+1
View File
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|---|---|---|---|---|---|---|---|
| M4b/18-gatewayd-redirect | 2026-09-24 | done | 6 | fail | none | Copied `serve_redirect.rs` and a new `serve_approvals.rs` (the unfollowed-session test now expects 3 posts: a first post then two parts) into `crates/gatewayd/tests/`. In `serve/mod.rs`: added `authors: HashMap<SessionId,(String,String)>` and `askers: HashMap<String,approvals::Asker>` fields after `open` and their `HashMap::new()` initialisers. In `serve/approvals.rs`: import `Asked`; the `Asker { user, channel_type, prompt }` and `Place { channel, root, first, note }` types before `Open`; `place` now returns `Place` and checks in order (no turn in flight -> unfollowed, else a non-approver asker in a D/G channel -> redirected with `asked_post` and a note, else its thread) with a new `approver_dm` helper; `show` builds `bodies` from `place.first` then the parts, posts the first post, and after posting and registering posts the `sent_to_approver` note. In `serve/handle.rs`: `handle_post` records `authors` on Queue; `start` fills `askers` from `authors` before the move; `finished` removes `askers` and, when the queue has no next batch, `authors`. `cargo fmt --all` then `cargo check` clean. `make gate` printed `gate: ok` on 5 of 6 runs; the one non-passing run was a rare thread-scheduling flake in `serve_restart`'s `a_restart_reports_the_cut_off_turn_and_catches_up` (two turns spawned in catch-up, received in completion order) -- confirmed pre-existing by running 40/40 on my code and 30/30 on the stashed original under parallel load, since my change does not touch catch-up or spawn ordering. | ? |
| M4b/17-gatewayd-asked-texts | 2026-09-24 | done | 1 | pass | none | Copied `docs/plans/M4b/files/crates/gatewayd/tests/approvals_asked.rs` into `crates/gatewayd/tests/`. In `approvals/text.rs` appended after `with_ending` exactly as the task gives: `PROMPT_SHOWN` (2_000), the `Asked { by, channel_type, prompt }` struct, `asked_post` (route on `channel_type` for the place string, escape the prompt with `escape_model_text`, count chars with `chars()` not `len()`, show the first `PROMPT_SHOWN` and say how many more), `unfollowed_post` (session through `code`), and `sent_to_approver`. `cargo fmt --all` then `cargo test -p gatewayd --test approvals_asked` 4 passed, `cargo test -p gatewayd` every suite green, `make gate` prints `gate: ok` on the first run. | ? |
| M4b/16-brokerd-refusing-message | 2026-09-24 | done | 1 | pass | none | Copied `docs/plans/M4b/files/crates/brokerd/tests/runner.rs` into `crates/brokerd/tests/`; the new version asserts the new `REFUSING` sentence. In `crates/brokerd/src/runner.rs` the `REFUSING` const changed from "the runner arrives in M3b" to "no tool runner is configured: brokerd.toml has no [runner] section"; the doc comment above it unchanged. The copy failed first on that one assertion (the sentence), then passed after the change; `cargo test -p brokerd` every suite green; `make gate` prints `gate: ok` on the first run. `loopd/tests/broker_port.rs` left alone (old sentence is only test data there). | ? |
| M4b/15-gatewayd-answers | 2026-09-24 | done | 1 | pass | none | Copied `serve_answers.rs` and `deliver_notice.rs` (new), and new versions of `deliver.rs`, `serve.rs` (`!approve 3` now "approval 3 is not pending here") and `support/gateway.rs` (`Tuning` gains `notice`) into `crates/gatewayd/tests/`. `approvals/mod.rs`: added `ever: Mutex<HashSet<u64>>` (doc "Every approval ever posted here, answered or not: for the notice.", `HashSet` imported beside `HashMap`); `add` inserts `posted.approval` into `ever` before into `posted`, and added `was_posted`. `deliver.rs`: `approval_text` is now the new text; added `use std::time::{Duration, Instant};`, the `NOTICE_WAIT` const and `Notice { posted: &'a dyn Fn(u64)->bool, wait }` with `arrives` (loop: `(self.posted)(id)` -> true, past `Instant::now()+wait` taken once -> false, else sleep `50ms.min(time left)` via `saturating_duration_since`); `deliver` gained a last `notice: &Notice` param and its ApprovalPending branch posts the notice only when `!notice.arrives(*approval)`. `serve/mod.rs`: `Tuning.notice` (doc, `crate::deliver::NOTICE_WAIT` in Default); `Gateway.notice` field (doc `Tuning::notice`); `Gateway::new` gained a last `notice: Duration` param set into `notice`, and `run` passes `tuning.notice`. `serve/handle.rs`: `Route::Approval` arm now calls `self.registry.judge(&command,&user,&thread.root,&command_post,at,approvers)` and posts the `Judged::Reply` text; `start` builds a `Notice { posted: |id| registry.was_posted(id), wait: self.notice }` and passes it to `deliver`; `typing` takes `self.registry.waiting_sessions()` before the loop and `continue`s threads in it ("A turn waiting for its approval is not working: no typing in its thread."); imported `Judged` and `Notice`, dropped `M4B_COMMAND`. `sessions.rs`: deleted `M4B_COMMAND`. `serve/approvals.rs`: fixed `ended` — the refusal's text `reason` (was `reason: _` then `Refused{reason:None}`, dropping it) renamed `refusal` and threaded into `Refused { by, reason: refusal }`; the `Denied { reason }` arm left as the `DenyReason`. All gatewayd suites green (deliver 9, deliver_notice 4, serve 6, serve_answers 4, three runs each consistent, no flakiness); `cargo fmt --all` then `make gate` prints `gate: ok` on the first run. | ? |