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

@ -34,7 +34,18 @@ use crate::client;
pub enum SocketReply {
Ok,
Err(String),
Message { from: String, body: String },
/// `id` is the broker's row id — not surfaced to claude but
/// useful for harness-side bookkeeping (not used in this module
/// today; the bin loops drive ack via `AckTurn` instead of
/// per-id). `redelivered` triggers the "may already be handled"
/// hint in `format_recv` so claude sees it when draining the
/// inbox in-turn.
Message {
from: String,
body: String,
id: i64,
redelivered: bool,
},
Empty,
Status(u64),
QuestionQueued(i64),
@ -54,7 +65,17 @@ impl From<hive_sh4re::AgentResponse> for SocketReply {
match r {
hive_sh4re::AgentResponse::Ok => Self::Ok,
hive_sh4re::AgentResponse::Err { message } => Self::Err(message),
hive_sh4re::AgentResponse::Message { from, body } => Self::Message { from, body },
hive_sh4re::AgentResponse::Message {
from,
body,
id,
redelivered,
} => Self::Message {
from,
body,
id,
redelivered,
},
hive_sh4re::AgentResponse::Empty => Self::Empty,
hive_sh4re::AgentResponse::Status { unread } => Self::Status(unread),
hive_sh4re::AgentResponse::Recent { rows } => Self::Recent(rows),
@ -81,7 +102,17 @@ impl From<hive_sh4re::ManagerResponse> for SocketReply {
match r {
hive_sh4re::ManagerResponse::Ok => Self::Ok,
hive_sh4re::ManagerResponse::Err { message } => Self::Err(message),
hive_sh4re::ManagerResponse::Message { from, body } => Self::Message { from, body },
hive_sh4re::ManagerResponse::Message {
from,
body,
id,
redelivered,
} => Self::Message {
from,
body,
id,
redelivered,
},
hive_sh4re::ManagerResponse::Empty => Self::Empty,
hive_sh4re::ManagerResponse::Status { unread } => Self::Status(unread),
hive_sh4re::ManagerResponse::QuestionQueued { id } => Self::QuestionQueued(id),
@ -117,10 +148,22 @@ pub fn format_ack(resp: Result<SocketReply, anyhow::Error>, tool: &str, ok_msg:
}
/// Format helper for `recv` tools: `Message` → from + body block;
/// `Empty` → marker; anything else surfaces as an error.
/// `Empty` → marker; anything else surfaces as an error. When the
/// broker tags the row as `redelivered` (popped before, never acked,
/// resurfaced after a harness restart) a short banner is prepended
/// so claude knows the side-effects of any previous handling may
/// already have happened.
pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
match resp {
Ok(SocketReply::Message { from, body }) => format!("from: {from}\n\n{body}"),
Ok(SocketReply::Message {
from,
body,
redelivered,
..
}) => {
let banner = if redelivered { REDELIVERY_HINT } else { "" };
format!("{banner}from: {from}\n\n{body}")
}
Ok(SocketReply::Empty) => "(empty)".into(),
Ok(SocketReply::Err(m)) => format!("recv failed: {m}"),
Ok(other) => format!("recv unexpected response: {other:?}"),
@ -128,6 +171,14 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
}
}
/// Header prepended to message bodies that were popped by a prior
/// harness session, never acked (turn crash / OOM / restart), and
/// resurfaced by `RequeueInflight` on this session's boot. Same
/// string surfaces in the wake prompt (see the bin loops) and the
/// in-turn `recv` tool result so claude sees the warning either way.
pub const REDELIVERY_HINT: &str =
"[redelivered after harness restart — may already be handled]\n";
/// Format helper for `get_loose_ends`: renders a short bulleted list
/// of pending approvals + questions + reminders. Empty list collapses
/// to a clear marker so claude doesn't go hunting for a payload that