feat: hint to do other work when recv/bash_status waits time out

When an agent parks on a long-poll that expires with nothing to show,
nudge it to spend the idle time on other useful work instead of
immediately re-blocking on the same call.

- recv: when wait_seconds > 0 and the inbox is empty at timeout, the
  '(empty)' result now carries IDLE_WAIT_HINT. Immediate peeks
  (no/zero wait) are unchanged.
- bash_status: when wait_seconds > 0 and the task is still
  pending/running at timeout, append BASH_IDLE_WAIT_HINT. Finished
  tasks and no-wait calls are unchanged.

Both thread a 'waited' flag into the formatter so the hint only fires
on an actual wait-timeout. Unit tests cover both. Implements #1411.
This commit is contained in:
iris 2026-06-05 19:58:22 +02:00 committed by mara
commit d34c594f2b
2 changed files with 94 additions and 13 deletions

View file

@ -119,11 +119,27 @@ fn render_bash_run(id: &str, resp: Result<DaemonResponse>) -> String {
}
}
/// Appended to a `status` result when the caller parked on a
/// `wait_seconds` poll that expired while the task was still running —
/// nudges the model to spend the idle time on other work instead of
/// immediately re-waiting on the same task.
const BASH_IDLE_WAIT_HINT: &str = "\n\nThe task is still running — your wait timed out before it \
finished. If you have other useful work, do that and check back later (the task keeps running, and \
a wake fires when it completes) rather than immediately re-waiting.";
/// Turn a `DaemonResponse` from a `BashStatus` call into the string
/// claude sees as the tool result.
fn render_bash_status(id: &str, resp: Result<DaemonResponse>) -> String {
/// claude sees as the tool result. When `waited` is set (the call
/// parked on a `wait_seconds` poll) and the task is still non-terminal,
/// [`BASH_IDLE_WAIT_HINT`] is appended.
fn render_bash_status(id: &str, resp: Result<DaemonResponse>, waited: bool) -> String {
match resp {
Ok(DaemonResponse::Ok { payload }) => format_task(id, &payload),
Ok(DaemonResponse::Ok { payload }) => {
let mut out = format_task(id, &payload);
if waited && matches!(payload["status"].as_str(), Some("pending" | "running")) {
out.push_str(BASH_IDLE_WAIT_HINT);
}
out
}
Ok(DaemonResponse::Error { message }) => message,
Err(e) => format!("bash bridge error: {e:#}"),
}
@ -215,11 +231,12 @@ impl BashMcp {
)]
async fn status(&self, Parameters(args): Parameters<BashStatusArgs>) -> String {
let id = args.id.clone();
let waited = args.wait_seconds.is_some_and(|w| w > 0);
let req = DaemonRequest::BashStatus {
id: args.id,
wait_seconds: args.wait_seconds,
};
render_bash_status(&id, round_trip(req).await)
render_bash_status(&id, round_trip(req).await, waited)
}
}
@ -240,3 +257,34 @@ async fn main() -> Result<()> {
service.waiting().await?;
Ok(())
}
#[cfg(test)]
mod status_hint_tests {
use super::{BASH_IDLE_WAIT_HINT, render_bash_status};
use anyhow::Result;
use hive_bash_mcp::protocol::DaemonResponse;
fn ok(status: &str) -> Result<DaemonResponse> {
Ok(DaemonResponse::Ok {
payload: serde_json::json!({ "status": status, "started_at": 1 }),
})
}
#[test]
fn running_task_after_wait_appends_idle_hint() {
let out = render_bash_status("t1", ok("running"), true);
assert!(out.contains(BASH_IDLE_WAIT_HINT));
}
#[test]
fn running_task_without_wait_has_no_hint() {
let out = render_bash_status("t1", ok("running"), false);
assert!(!out.contains(BASH_IDLE_WAIT_HINT));
}
#[test]
fn finished_task_after_wait_has_no_hint() {
let out = render_bash_status("t1", ok("done"), true);
assert!(!out.contains(BASH_IDLE_WAIT_HINT));
}
}