From a443108be5f6457653b5f26ffd0821ffe14f1829 Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 31 May 2026 15:50:57 +0200 Subject: [PATCH] hive-sh4re + docs: extract broker delivery/ack/requeue prose (#717 batch 1) --- docs/conventions.md | 37 ++++++++++++++++++++++++ hive-sh4re/src/lib.rs | 65 ++++++++++++------------------------------- 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/docs/conventions.md b/docs/conventions.md index f5370548..c92bcb56 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -54,6 +54,43 @@ each frame carries a `seq` field for the snapshot-dedupe dance — change them in one place. The dashboard event vocabulary lives in `hive-c0re::dashboard_events::DashboardEvent`. +### Broker delivery + ack cycle + +`AgentRequest::Recv` is the only path that delivers messages to an +agent. Always returns a list (`Messages { messages }`) — empty when +nothing's pending, single-pop when `max = None` (default 1, the +single-message behaviour), batched up to `max` when caller asks for +more (server-side cap is 32; values above clamp silently). +`wait_seconds` long-polls for the first message; once one arrives — +or one is already pending — the call drains up to `max` in total +before returning, so a single `Recv` call coalesces a burst. + +Per-row bookkeeping inside the broker: + +- `delivered_at = NOW` set on every popped row. +- Each recipient has an in-memory `unacked_ids` list of every row + delivered since the last `AckTurn`. +- `redelivered = true` on a row if `RequeueInflight` resurfaced it + (the harness prepends a "may already be handled" hint when this + flag is set so the per-message warning is visible). + +`AgentRequest::AckTurn` closes out the in-memory list — the harness +fires it after `TurnOutcome::Ok`, marking every message popped since +the last ack as fully handled. Claude doesn't see this surface; it's +strictly a harness↔broker pairing. On `TurnOutcome::Failed` the +harness intentionally skips the ack so the unacked rows stay +in-flight in the DB and get picked up by the next requeue sweep. + +`AgentRequest::RequeueInflight` is the recovery pair: fired by the +harness exactly once at boot, before the serve loop starts. Catches +the crashed-mid-turn / OOM-killed / container-restarted cases where +a previous harness session popped messages but never drove them to +a clean turn-end. Resets `delivered_at` back to NULL on every +unacked row (so the next `Recv` pops them again), and remembers +each id in a per-recipient in-memory set so the next `Recv` can tag +the row with `redelivered: true`. Idempotent + cheap when there's +nothing in flight, so the at-boot fire is unconditional. + ## Async forms Dashboard + per-agent mutating forms carry `data-async`; a delegated diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 990c0a62..5162145b 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -241,26 +241,20 @@ pub struct InboxRow { pub in_reply_to: Option, } -/// One delivered message in a `Recv` response. The unified -/// `Recv { max }` always returns a `Vec` — single -/// pop = a one-element vec, batch = up to `max` elements, idle = -/// empty. Each row carries the broker's id + redelivered flag so the -/// harness can drive `AckTurn` and surface the "may already be -/// handled" hint per-row. +/// One delivered message in a `Recv` response. +/// See `docs/conventions.md::Broker delivery + ack cycle` for the +/// full delivery/ack/requeue story. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeliveredMessage { pub from: String, pub body: String, - /// Broker row id, mirrored from the `Delivery` struct. Opaque to - /// claude but tracked by the harness so the broker's in-memory - /// unacked list can be drained on `AckTurn`. Marked `default` for - /// wire backwards-compat — pre-feature peers parse to 0. + /// Broker row id, tracked by the harness for `AckTurn`. Opaque to + /// claude. `default` for wire backwards-compat. #[serde(default)] pub id: i64, - /// `true` when this row was previously popped, never acked, and - /// resurfaced by `RequeueInflight`. The format helper prepends the - /// "may already be handled" hint to the rendered body so claude - /// sees the warning per-message in the batch. + /// `true` if this row was resurfaced by `RequeueInflight` (previously + /// popped, never acked). Formatter prepends a "may already be handled" + /// hint when set. #[serde(default)] pub redelivered: bool, /// Row-id of the message this is a reply to, if any. @@ -371,22 +365,12 @@ pub enum AgentRequest { #[serde(default, skip_serializing_if = "Option::is_none")] in_reply_to: Option, }, - /// Pop pending messages from this agent's inbox. Always returns - /// a list (`Messages { messages }`) — empty when nothing's - /// pending. `max` caps the batch size (default 1 = single-message - /// behaviour, server-side cap 32). `wait_seconds` long-polls for - /// the first message; once one arrives (or one is already - /// pending), the call drains up to `max` in total before - /// returning. Same delivery + ack bookkeeping per row as before: - /// `delivered_at = NOW`, tracked on the per-recipient - /// `unacked_ids` list (the next `AckTurn` closes them out), and - /// each row carries `redelivered = true` if `RequeueInflight` - /// resurfaced it. + /// Pop pending messages from this agent's inbox. + /// Delivery + ack cycle: see + /// `docs/conventions.md::Broker delivery + ack cycle`. Recv { #[serde(default)] wait_seconds: Option, - /// Maximum number of messages to pop. None = 1 (single). - /// Server-side cap is 32; values above clamp silently. #[serde(default)] max: Option, }, @@ -498,27 +482,14 @@ pub enum AgentRequest { /// row. The manager surface uses the same wire variant but /// accepts any id. CancelLooseEnd { kind: CancelLooseEndKind, id: i64 }, - /// Mark every message popped by this agent since the last `AckTurn` - /// as fully handled. Fired by the harness after `TurnOutcome::Ok` - /// — claude doesn't see this surface, it's harness↔broker only. - /// On `TurnOutcome::Failed` the harness intentionally skips this - /// call, so the unacked rows stay in-flight in the DB and get - /// requeued by the next `RequeueInflight` on harness boot. Tracks - /// the popped-id list in-memory on the broker side; no payload - /// needed (the broker knows which ids it handed to this - /// recipient). + /// Mark every message popped since the last `AckTurn` as handled. + /// Harness↔broker pairing fired after `TurnOutcome::Ok`. See + /// `docs/conventions.md::Broker delivery + ack cycle`. AckTurn, - /// Requeue every message the broker handed to this agent that - /// never got acked. Fired by the harness exactly once at boot, - /// before entering the serve loop — catches the - /// crashed-mid-turn / OOM-killed / container-restarted cases - /// where a previous harness session popped messages but never - /// drove them to a clean turn-end. Resets `delivered_at` on each - /// row back to NULL (so the next `Recv` pops it) and remembers - /// the id in a per-recipient in-memory set so the next `Recv` - /// can tag the message with `redelivered: true` (the harness - /// then prepends a "may already be handled" hint to the wake - /// prompt). Idempotent + cheap when there's nothing in flight. + /// Requeue every popped-but-unacked message back into the inbox. + /// Harness fires this once at boot to recover from + /// crashed-mid-turn sessions. See + /// `docs/conventions.md::Broker delivery + ack cycle`. RequeueInflight, }