From 34cc68bdfb27e93b2f0f7bf3f18c85f555de2ab8 Mon Sep 17 00:00:00 2001 From: damocles Date: Mon, 1 Jun 2026 10:37:20 +0200 Subject: [PATCH] docs: migrate scheduled-prompts worker + dashboard-events design prose to docs --- docs/approvals.md | 20 ++++++++++++ docs/web-ui/shape.md | 8 +++++ hive-c0re/src/dashboard_events.rs | 29 +++------------- hive-c0re/src/scheduled_prompts_worker.rs | 40 +++-------------------- 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/docs/approvals.md b/docs/approvals.md index 75ad859b..01736bf6 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -142,6 +142,26 @@ One-shot rows fire once (if past due, on the next worker pass) and are deleted b `targets` is its own table (`scheduled_prompt_targets`) so partial cancellation flips a single row and the dashboard can show last-fired / last-result per recipient. Cancelling every target reaps the parent row on the next worker pass. +### Missing-target failure + +When a target name doesn't resolve to a known agent (container +destroyed, operator typo, etc.) the worker: + +1. Records `last_result = "no such agent: "` on the + per-target row. +2. Sends a single advisory `Message` from `system` to `operator` + naming the schedule, target, and reason. +3. Continues fanning out to the other live targets. + +Transient broker errors (sqlite lock contention, etc.) get the same +`last_result` annotation plus a `tracing::warn`, and then: + +- **Recurring rows** re-arm to the next interval slot — the retry + self-heals on the next worker pass. +- **One-shot rows** are deleted unconditionally after their single + fan-out pass; a broker error on a one-shot is not retried (the + operator advisory and `last_result` are the only audit trail). + ### Destroy semantics `HostRequest::Destroy { name, purge }` is the lifecycle tear-down, diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index 15817abd..29dce188 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -46,6 +46,14 @@ ~200 broker messages wrapped in `{ seq, events }`) on the dashboard and `GET /events/history` (last 2000 `LiveEvent`s also wrapped in `{ seq, events }`) on the agent. + **One unified channel**: browsers cap concurrent SSE + connections per origin (~6 in Chrome). Using one channel per + domain would exhaust this budget on a live hive; dispatching + by `kind` on the client is a one-liner. Per-domain splits are + reserved for high-volume sub-streams most consumers skip (none + exist yet). The broker's intra-process channel stays separate + from the dashboard channel to avoid coupling `recv_blocking_batch` + (hot path inside the harness turn loop) to presentation concerns. **SSE multiplexing**: the dashboard uses a `SharedWorker` (`stream-worker.js`) to hold one upstream `EventSource` per URL. All same-origin tabs share this worker diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index 83a3fcac..fd05b19d 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -1,27 +1,8 @@ -//! Unified dashboard event channel. -//! -//! Anything the browser wants to react to in near-real-time flows through -//! `Coordinator.dashboard_events`. Each event is stamped with a monotonic -//! per-process `seq` so the client can dedupe its buffered live traffic -//! against snapshot/history responses (drop frames with -//! `seq <= snapshot.seq`). -//! -//! Why one channel instead of one-per-domain: browsers cap concurrent -//! SSE connections per origin (~6 in chrome) and dispatch-by-kind on the -//! client is a one-liner. Splits get reserved for high-volume sub-streams -//! that most consumers don't care about (none yet). -//! -//! Message-broker traffic (`Sent` / `Delivered`) lives on this channel -//! too. A background forwarder task in `main.rs` subscribes to the broker -//! and re-emits each `MessageEvent` as a `DashboardEvent::Sent` / -//! `DashboardEvent::Delivered` with a freshly-stamped seq. Keeping the -//! broker's intra-process channel separate avoids coupling the broker -//! (used by `recv_blocking_batch` inside the harness loop) to dashboard -//! presentation concerns. -//! -//! New mutation kinds (approval added/resolved, question added/answered, -//! transient changed, etc.) land here as additional variants. The client -//! dispatches by `kind` and updates the relevant section. +//! Unified dashboard event channel — all near-real-time browser events +//! flow through `Coordinator.dashboard_events`. Each event carries a +//! monotonic `seq` for client-side dedupe against `/api/state` snapshots. +//! Design rationale (single channel, broker forwarder isolation): +//! `docs/web-ui/shape.md::One unified channel`. use serde::Serialize; diff --git a/hive-c0re/src/scheduled_prompts_worker.rs b/hive-c0re/src/scheduled_prompts_worker.rs index b241f164..611a57d4 100644 --- a/hive-c0re/src/scheduled_prompts_worker.rs +++ b/hive-c0re/src/scheduled_prompts_worker.rs @@ -1,39 +1,7 @@ -//! Background loop that drains due `scheduled_prompts` rows -//! and fans the body out as inbox `Message`s to each active -//! target. Mirrors `reminder_scheduler::spawn` shape: -//! single `spawn(coord)` entry, 5s poll cadence, shutdown-aware. -//! -//! ## Catch-up semantics -//! -//! When hive-c0re comes back from being down, a recurring row -//! whose `next_fire_at` is well in the past would otherwise fire -//! N delayed pulses in a row. Instead we fire ONCE and let -//! `ScheduledPrompts::rearm` bump `next_fire_at` to the next -//! interval slot ≥ `now`, recording the skipped-cycle count in -//! the per-target `last_result` so operators see how many -//! firings were caught up rather than losing the signal. -//! -//! ## Missing-target failure -//! -//! When a target name doesn't resolve to a known agent (the -//! container has been destroyed, the operator typo'd a name, -//! etc.) the worker: -//! 1. records `last_result = "no such agent: "` against -//! the per-target row, -//! 2. sends a single advisory `Message` from `system` to -//! `operator` describing the schedule + target + reason, -//! 3. continues fanning out to the other (live) targets. -//! -//! Transient broker errors (sqlite lock contention, etc.) get -//! the per-target `last_result` annotated AND a `tracing::warn`, -//! but the post-fire bookkeeping treats the row the same way it -//! does on a clean fire: -//! - **recurring** rows re-arm — the next interval slot tries -//! the broker send again, so transient errors self-heal. -//! - **one-shots** delete unconditionally after their single -//! fan-out pass; a broker failure on a one-shot is NOT -//! retried (the operator advisory + `last_result` are the only -//! audit trail). +//! Background loop that drains due `scheduled_prompts` rows and fans +//! the body to each active target. 5s poll cadence, shutdown-aware. +//! Catch-up clamp, missing-target handling, and broker-error retry +//! semantics: `docs/approvals.md::Scheduled prompt worker`. use std::sync::Arc; use std::time::Duration;