gatewayd: approvals answered from Mattermost; typing paused; the notice

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-24 10:19:41 -07:00
parent af7137dafe
commit edf9ba3515
12 changed files with 472 additions and 25 deletions
+41 -5
View File
@@ -3,6 +3,7 @@
use std::os::unix::net::UnixStream;
use std::path::Path;
use std::time::{Duration, Instant};
use proto::{
Envelope, ErrorCode, Message, PROTOCOL_VERSION, Turn, TurnEvent, WireError, read_frame,
@@ -40,10 +41,36 @@ pub enum Outcome {
pub fn approval_text(approval: u64) -> String {
format!(
"waiting for approval {approval}: approve or deny it with `bxctl` (Mattermost approvals arrive in M4b)"
"waiting for approval {approval}: brokerd has not sent it here; answer it with bxctl approvals"
)
}
/// How long a turn waits for `brokerd` to bring its approval here before posting the notice.
pub const NOTICE_WAIT: Duration = Duration::from_secs(10);
/// Whether an approval was brought here (and so is posted already), and how long to wait for it.
pub struct Notice<'a> {
pub posted: &'a dyn Fn(u64) -> bool,
pub wait: Duration,
}
impl Notice<'_> {
fn arrives(&self, id: u64) -> bool {
let until = Instant::now() + self.wait;
loop {
if (self.posted)(id) {
return true;
}
if Instant::now() >= until {
return false;
}
std::thread::sleep(
Duration::from_millis(50).min(until.saturating_duration_since(Instant::now())),
);
}
}
}
/// "Error: <code>: <detail>"; the detail carries `loopd`'s runbook pointer when there is one.
pub fn error_text(error: &WireError) -> String {
// "Error: <code>: <detail>", where <code> is the snake_case name serde gives the ErrorCode
@@ -173,14 +200,23 @@ fn post(poster: &dyn Poster, thread: &Thread, text: String, log: &dyn Fn(&str))
}
/// Run a batch's turn and post what comes of it in its thread. A post that fails is logged.
pub fn deliver(poster: &dyn Poster, socket: &Path, batch: &Batch, log: &dyn Fn(&str)) {
// Post in the batch thread. An ApprovalPending event posts `approval_text(approval)` at once.
// Then: Answer -> every part of `split_answer`, in order; Refused -> `error_text`;
pub fn deliver(
poster: &dyn Poster,
socket: &Path,
batch: &Batch,
log: &dyn Fn(&str),
notice: &Notice,
) {
// Post in the batch thread. An ApprovalPending event waits up to `notice.wait` for `brokerd` to
// bring the approval through `gateway.sock`; only when it does not, it posts the notice. Then:
// Answer -> every part of `split_answer`, in order; Refused -> `error_text`;
// LoopDown(why) -> log "gatewayd: <session>: <why>" and post LOOP_DOWN. A post that fails is
// logged, exactly "gatewayd: cannot post in <channel> (thread <root>): <error>", and the rest
// goes on.
let outcome = run_turn(socket, batch, &mut |event| {
if let TurnEvent::ApprovalPending { approval, .. } = event {
if let TurnEvent::ApprovalPending { approval, .. } = event
&& !notice.arrives(*approval)
{
post(poster, &batch.thread, approval_text(*approval), log);
}
});