diff --git a/crates/loopd/src/broker_port.rs b/crates/loopd/src/broker_port.rs index 11e7f4d..f0a2596 100644 --- a/crates/loopd/src/broker_port.rs +++ b/crates/loopd/src/broker_port.rs @@ -26,6 +26,9 @@ pub const MAX_PENDING_WAIT: Duration = Duration::from_secs(24 * 60 * 60); /// longer: `brokerd` waits 170 s for `memoryd`, whose Honcho chats have 150 s (M5c spec, /// section 9). pub const ASK_MEMORY_WAIT: Duration = Duration::from_secs(180); +/// The longest `BrokerPort` waits for a `consult` answer, unless `[broker] timeout_ms` is +/// longer: `brokerd`'s `[consult] timeout_ms` defaults to 180 s (M6 spec, section 4.3). +pub const CONSULT_WAIT: Duration = Duration::from_secs(180); /// The runbook entry every outage line ends with. pub const POINTER: &str = "see docs/runbook.md#broker-unavailable"; @@ -63,6 +66,7 @@ pub struct BrokerPort { socket: PathBuf, timeout: Duration, ask_timeout: Duration, + consult_timeout: Duration, pending_cap: Duration, log: Box, } @@ -73,6 +77,7 @@ impl BrokerPort { socket, timeout, ask_timeout: ASK_MEMORY_WAIT, + consult_timeout: CONSULT_WAIT, pending_cap: MAX_PENDING_WAIT, log: Box::new(|l| eprintln!("{l}")), } @@ -88,10 +93,12 @@ impl BrokerPort { } /// The wait for one call of `tool`, used wherever the port waits for that call: the timeout, - /// or for `ask_memory` the longer of the timeout and its own wait. + /// or for `ask_memory` and `consult` the longer of the timeout and that tool's own wait. pub fn timeout_for(&self, tool: &str) -> Duration { if tool == crate::memory::ASK_MEMORY { self.timeout.max(self.ask_timeout) + } else if tool == crate::consult::CONSULT { + self.timeout.max(self.consult_timeout) } else { self.timeout } @@ -114,6 +121,7 @@ impl BrokerPort { socket, timeout, ask_timeout: ASK_MEMORY_WAIT, + consult_timeout: CONSULT_WAIT, pending_cap: MAX_PENDING_WAIT, log, } diff --git a/crates/loopd/tests/broker_port_consult_wait.rs b/crates/loopd/tests/broker_port_consult_wait.rs new file mode 100644 index 0000000..c67d130 --- /dev/null +++ b/crates/loopd/tests/broker_port_consult_wait.rs @@ -0,0 +1,22 @@ +//! How long `BrokerPort` waits for `consult`: 180 s, or `[broker] timeout_ms` if that is longer. +//! Do not edit. + +use std::time::Duration; + +use loopd::broker_port::{BrokerPort, CONSULT_WAIT}; + +fn quiet(timeout: Duration) -> BrokerPort { + BrokerPort::with_log("unused.sock".into(), timeout, Box::new(|_| {})) +} + +#[test] +fn consult_waits_one_hundred_and_eighty_seconds() { + assert_eq!(CONSULT_WAIT, Duration::from_secs(180)); + let port = BrokerPort::new("unused.sock".into(), Duration::from_secs(120)); + assert_eq!(port.timeout_for("consult"), CONSULT_WAIT); + assert_eq!(port.timeout_for("recall"), Duration::from_secs(120)); + let port = BrokerPort::new("unused.sock".into(), Duration::from_secs(300)); + assert_eq!(port.timeout_for("consult"), Duration::from_secs(300)); + let port = quiet(Duration::from_secs(120)); + assert_eq!(port.timeout_for("consult"), CONSULT_WAIT, "with_log as new"); +} diff --git a/docs/implementer-log.md b/docs/implementer-log.md index 6131aeb..72df0a7 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone. | Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model | |---|---|---|---|---|---|---|---| +| M6b/02-loopd-consult-wait | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/broker_port_consult_wait.rs` from `docs/plans/M6b/files/`. Step 2 failed as expected: E0432 unresolved `CONSULT_WAIT`. Added `CONSULT_WAIT` const (180 s) after `ASK_MEMORY_WAIT` in `broker_port.rs`. Added a `consult_timeout` field to the struct and to both `new` and `with_log` constructors (`with_ask_wait` and `with_pending_cap` use `..self` and need no edit). Extended `timeout_for` so `consult` returns `self.timeout.max(self.consult_timeout)` (floor, not replacement); a configured 300 s wait stays 300 s. `ASK_MEMORY_WAIT` and its arm unchanged; `broker_port_ask_wait.rs` and `config.rs` untouched. `cargo check -p loopd` clean, no warning; `wc -l` prints 270, under 500. All suites pass: `broker_port_consult_wait` 1, `broker_port_ask_wait` 4 (`the_wait_for_each_tool` still passes). `make gate` printed `gate: ok` with 1593 tests (1592 before, plus 1). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6b/01-loopd-consult-registry | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/consult_registry.rs` from `docs/plans/M6b/files/`. Created `crates/loopd/src/consult.rs` with `CONSULT` const and `schema()` returning the `consult` `ToolSchema`. Added `pub mod consult;` to lib.rs after `pub mod config;`. In tools.rs `m5c()` pushed the non-core `consult` entry after the `subagent` entry (`Registry::new(entries)` unchanged). `cargo check -p loopd` clean, no warning; `wc -l` prints 21 for consult.rs, 481 for tools.rs, both under 500. All suites pass: `consult_registry` 2, `subagent_registry` 2, `ask_memory_tool`, `tools` 11, `baseline` 6. `make gate` printed `gate: ok` with 1592 tests (1590 before, plus 2). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6a/05-brokerd-state-inherit | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/brokerd/tests/state_subagent.rs` from `docs/plans/M6a/files/`. Step 2 (`cargo test -p brokerd --test state_subagent`) failed as expected: `a_new_subagent_copies_the_parents_untrusted_flag` (child was default private/trusted) and `a_nested_subagent_copies_through_a_missing_middle_child` (leaf was Private not Secret) failed, `a_plain_session_still_creates_nothing_on_read` passed. Applied I1 in state.rs `read`: replaced the `NotFound` early return with `return inherit_or_default(self, session)`. Applied I2 after `impl StateStore` (after `persist`'s and the `impl`'s closing braces, before `/// One line on disk`): added `inherit_or_default` which calls `parent_of_sub`, returns default when `None`, else `store.read(&parent)?` then `store.raise(session, SessionState::default(), Label{class: inherited.taint, untrusted: inherited.untrusted})`, and `parent_of_sub` which strips `sub-`, `rsplit_once('-')`, requires non-empty ASCII-digit suffix and builds `SessionId::new(parent)`. Did not touch `raise` or `persist`. `cargo check -p brokerd` clean, no warning; `wc -l` prints 176 for state.rs, under 500. All suites pass: `state_subagent` 3, `state` 9. `make gate` printed `gate: ok` with 1590 tests (1587 before, plus 3). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b | | M6a/04-loopd-scheduler-skip | 2026-10-03 | done | 1 | pass | none | Copied the given test `crates/loopd/tests/scheduler_subagent.rs` from `docs/plans/M6a/files/`. Step 2 (`cargo test -p loopd --test scheduler_subagent`) failed as expected: both tests failed, `work` not empty (a `Heartbeat` and a `Job { name: "mail", session: "job-mail-20261003t0700" }`) and `requests_to(CHAT)` not empty while the child was held. Applied H1 in channel.rs: added `holds_prefix` after `hold`'s closing `}` inside `impl Context`, locking `busy` like `hold` and returning whether any id starts with the prefix. Applied K2 in scheduler.rs `heartbeat`: after the `if !due { return; }` block and before `SessionId::new("heartbeat")`, `if ctx.holds_prefix("sub-") { return; }`. Applied K1 in scheduler.rs `tick`: immediately after `self.heartbeat(...)` and before `jobs_dir`, `if ctx.holds_prefix("sub-") { return work; }` so leftover-close on the first tick is already done. `cargo check -p loopd` clean, no warning; `wc -l` prints 307 channel.rs, 310 scheduler.rs, both under 500. All suites pass: `scheduler_subagent` 2, `scheduler_heartbeat` 6, `scheduler_cron` 6, `scheduler_heartbeat_failed` 1. `make gate` printed `gate: ok` with 1587 tests (1585 before, plus 2). No new dependency, so `Cargo.lock` unchanged. | ornith-1.5-35b-a3b |