Debug collection: hold_open fix, grants.rs refactor, and DEBUG-HANDOFF findings

Debug collection commit for the brokerd admin-test hang investigation (M3a
task 19). Contains:

- crates/brokerd/src/broker.rs: hold_open helper (HOLD_OPEN = 2s read-timeout
  loop) applied after forbid and after the final send in broker::handle
- crates/brokerd/src/admin.rs: hold_open applied after forbid and after the
  final send in admin::handle
- crates/brokerd/src/grants.rs: check_tool_constraints refactor (match guard
  instead of nested if)
- docs/M3a/DEBUG-HANDOFF.md: investigation results added (310 runs, zero
  hangs reproduced; stalled fsync cannot be fixed without dropping durability)

The implementer log row was committed separately (a6d81d9).

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-22 15:39:20 -07:00
parent a6d81d941b
commit d21baa2954
4 changed files with 128 additions and 10 deletions
+23
View File
@@ -25,6 +25,10 @@ use crate::runner::Runtime;
/// The failure message sent when a peer leaves at the last look, after the table entry is taken.
pub const GONE: &str = "the requester went away";
/// How long to keep a socket open after the final frame, so the peer can finish reading before its
/// end of the pair is half-closed.
const HOLD_OPEN: Duration = Duration::new(2, 0);
/// A line printer: one line per call, owned by the broker.
pub type Log = Box<dyn Fn(&str) + Send + Sync>;
@@ -185,6 +189,23 @@ pub fn alive(stream: &UnixStream) -> bool {
}
}
/// Keep the socket open after the final frame, until the peer closes it or the timeout elapses.
/// Dropping the peer's end makes its next `set_read_timeout` fail on macOS, so we hold the read
/// half open for as long as the peer might still be reading.
pub(crate) fn hold_open(stream: &mut UnixStream) {
if stream.set_read_timeout(Some(HOLD_OPEN)).is_err() {
return;
}
let mut byte = [0u8; 1];
loop {
match stream.read(&mut byte) {
Ok(0) => break, // the peer closed
Ok(_) => continue,
Err(_) => break,
}
}
}
/// Answer one call: decide, and either answer it or wait for an approval.
pub fn handle(mut stream: UnixStream, broker: &Broker) {
let Some(envelope) = read_request(&mut stream) else {
@@ -194,6 +215,7 @@ pub fn handle(mut stream: UnixStream, broker: &Broker) {
let msg = envelope.msg;
let Message::ToolRequest(request) = msg else {
forbid(broker, &mut stream, id, &msg, "broker.sock"); // 2. not a tool request
hold_open(&mut stream);
return;
};
@@ -208,6 +230,7 @@ pub fn handle(mut stream: UnixStream, broker: &Broker) {
};
if let Some(answer) = answer {
let _ = send(&mut stream, id, true, Message::ToolResponse(answer)); // 4. the final frame
hold_open(&mut stream);
}
}