broker: lease-style delivery — ack_turn + requeue_inflight close the no-drop loop

This commit is contained in:
damocles 2026-05-18 22:01:48 +02:00
parent 69a3ca7469
commit 690cb5ab5b
8 changed files with 684 additions and 35 deletions

View file

@ -121,6 +121,10 @@ async fn serve(
turn_lock: TurnLock,
) -> Result<()> {
tracing::info!(socket = %socket.display(), "hive-m1nd serve");
// Same boot-time recovery as hive-ag3nt — see that loop for the
// rationale. Manager-flavour socket so we requeue only manager
// inflight rows.
requeue_inflight(socket).await;
loop {
let recv: Result<ManagerResponse> =
// Explicit long-poll: see hive-ag3nt's serve loop for the
@ -134,7 +138,12 @@ async fn serve(
)
.await;
match recv {
Ok(ManagerResponse::Message { from, body }) => {
Ok(ManagerResponse::Message {
from,
body,
id: _,
redelivered,
}) => {
if from == SYSTEM_SENDER {
// Helper events (ApprovalResolved / Spawned / Rebuilt /
// Killed / Destroyed) — these are FYI for the manager;
@ -154,14 +163,14 @@ async fn serve(
// prompt body so claude sees it. Sender stays "system"
// so the wake prompt can label it as such.
}
tracing::info!(%from, %body, "manager inbox");
tracing::info!(%from, %body, %redelivered, "manager inbox");
let unread = inbox_unread(socket).await;
bus.emit(LiveEvent::TurnStart {
from: from.clone(),
body: body.clone(),
unread,
});
let prompt = format_wake_prompt(&from, &body, unread);
let prompt = format_wake_prompt(&from, &body, unread, redelivered);
bus.set_state(TurnState::Thinking);
let started_at = now_unix();
let started_instant = std::time::Instant::now();
@ -172,6 +181,12 @@ async fn serve(
};
turn::emit_turn_end(&bus, &outcome);
bus.set_state(TurnState::Idle);
// Ack only on a clean turn-end; Failed leaves the
// popped ids in-flight for the next boot's requeue.
// Mirrors hive-ag3nt; see that loop for full rationale.
if matches!(outcome, turn::TurnOutcome::Ok) {
ack_turn(socket).await;
}
if let Some(s) = &stats {
let ended_at = now_unix();
let duration_ms =
@ -228,8 +243,15 @@ async fn serve(
/// Per-turn user prompt. The role/tools/etc. is in the system prompt
/// (`prompts/manager.md` → `claude --system-prompt-file`); this is just
/// the wake signal. `unread` is the inbox depth after this message was
/// popped.
fn format_wake_prompt(from: &str, body: &str, unread: u64) -> String {
/// popped. `redelivered` adds a "may already be handled" banner above
/// the wake body when the broker resurfaced this row (see hive-ag3nt's
/// `format_wake_prompt` for the full story).
fn format_wake_prompt(from: &str, body: &str, unread: u64, redelivered: bool) -> String {
let banner = if redelivered {
hive_ag3nt::mcp::REDELIVERY_HINT
} else {
""
};
let pending = if unread == 0 {
String::new()
} else {
@ -237,7 +259,39 @@ fn format_wake_prompt(from: &str, body: &str, unread: u64) -> String {
"\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)"
)
};
format!("Incoming message from `{from}`:\n---\n{body}\n---{pending}")
format!("{banner}Incoming message from `{from}`:\n---\n{body}\n---{pending}")
}
/// Best-effort: tell the broker every message popped during the turn
/// is now handled. Mirror of `hive-ag3nt::ack_turn` on the manager
/// surface.
async fn ack_turn(socket: &Path) {
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::AckTurn).await {
Ok(ManagerResponse::Ok) => {}
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "ack_turn rejected by broker");
}
Ok(other) => {
tracing::warn!(?other, "ack_turn unexpected response");
}
Err(e) => tracing::warn!(error = ?e, "ack_turn transport error"),
}
}
/// Boot-time recovery: ask the broker to resurface any inflight (popped
/// but not acked) messages so the next `Recv` re-delivers them with
/// the redelivery banner. Mirror of `hive-ag3nt::requeue_inflight`.
async fn requeue_inflight(socket: &Path) {
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::RequeueInflight).await {
Ok(ManagerResponse::Ok) => {}
Ok(ManagerResponse::Err { message }) => {
tracing::warn!(%message, "requeue_inflight rejected by broker");
}
Ok(other) => {
tracing::warn!(?other, "requeue_inflight unexpected response");
}
Err(e) => tracing::warn!(error = ?e, "requeue_inflight transport error"),
}
}
async fn inbox_unread(socket: &Path) -> u64 {