hive-c0re + docs: extract scheduled_prompts prose (#715 batch 2)

This commit is contained in:
damocles 2026-05-31 17:07:57 +02:00 committed by mara
commit 50879cea5a
2 changed files with 23 additions and 42 deletions

View file

@ -125,6 +125,14 @@ kind-specific payload carrier.
inbox messages to each target at the scheduled time, recurring inbox messages to each target at the scheduled time, recurring
when `interval_seconds` is set. when `interval_seconds` is set.
### Scheduled prompt worker (catch-up clamp)
When hive-c0re comes back from being down, the worker sees rows whose `next_fire_at_unix` is well in the past. For recurring rows that would mean firing N delayed pulses in a row — spammy and useless. Instead the worker fires **once** per row and bumps `next_fire_at_unix` to the next interval slot ≥ `now`, recording how many cycles were skipped in `last_result` (per-target). Operators see "fired late, caught up from 17 skipped" instead of 17 wake-up storms.
One-shot rows fire once (if past due, on the next worker pass) and are deleted by the worker; recurring rows survive until cancelled.
`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.
### Destroy semantics ### Destroy semantics
`HostRequest::Destroy { name, purge }` is the lifecycle tear-down, `HostRequest::Destroy { name, purge }` is the lifecycle tear-down,

View file

@ -1,41 +1,14 @@
//! Scheduled prompts (closes #444). Persistent sqlite queue of //! Scheduled prompts: persistent sqlite queue of `(fire_at, targets,
//! `(fire_at, targets, body)` rows that the worker fans out as //! body)` rows that the worker fans out as broker `Message`s to each
//! broker `Message`s to each target's inbox at fire time. Recurring //! target's inbox at fire time. Recurring schedules carry
//! schedules carry `interval_seconds` and re-arm `next_fire_at` on //! `interval_seconds` and re-arm `next_fire_at` on delivery;
//! delivery; one-shots are reaped. //! one-shots are reaped.
//! //!
//! ## Three submit paths //! Schema + retention: `docs/persistence.md::scheduled_prompts /
//! //! scheduled_prompt_targets`. Submit paths (operator vs
//! - **Operator-direct** (`source = Operator`): the operator adds //! `ApprovalKind::SchedulePrompt`), catch-up clamp on resume, and
//! a schedule through the dashboard form. Lands in the table //! per-target tombstoning semantics: `docs/approvals.md::Scheduled
//! immediately, no approval gate. //! prompt worker (catch-up clamp)`.
//! - **Agent-requested** (`source = Approval { id }`): a sub-agent
//! (or the manager) submits a `RequestSchedulePrompt` through the
//! manager socket. An `ApprovalKind::SchedulePrompt` row is
//! queued; on approve, hive-c0re inserts the schedule row with
//! `source = Approval { id: approval_id }` so the audit trail
//! points back at the operator decision.
//! - **No self-target shortcut**: even agent-self schedules need
//! approval. The existing `remind` MCP tool stays the quick
//! self-wake path; this module is the bigger, multi-recipient,
//! operator-visible thing.
//!
//! ## Catch-up clamp (missed-while-down)
//!
//! When hive-c0re comes back from being down, the worker sees rows
//! whose `next_fire_at` is well in the past. For recurring rows
//! that would mean firing N delayed pulses in a row — spammy and
//! useless. Instead the worker fires ONCE per row and bumps
//! `next_fire_at` to the next interval slot ≥ `now`, recording how
//! many cycles were skipped in `last_result`. Operators see "fired
//! late, caught up from 17 skipped" instead of 17 wake-up storms.
//!
//! ## Per-target state
//!
//! `targets` is its own table so partial cancellation flips a
//! single row + so the dashboard can show last-fired / last-result
//! per recipient. Cancelling every target reaps the parent row on
//! the next worker pass.
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
@ -145,7 +118,7 @@ pub struct NewSchedule {
pub source: ScheduleSource, pub source: ScheduleSource,
} }
/// Partial-update payload for `ScheduledPrompts::update` (#474). /// Partial-update payload for `ScheduledPrompts::update`.
/// Every field is `Option<_>`; `None` keeps the existing value. /// Every field is `Option<_>`; `None` keeps the existing value.
/// The doubly-wrapped `Option<Option<u64>>` on `interval_seconds` /// The doubly-wrapped `Option<Option<u64>>` on `interval_seconds`
/// is intentional: outer `None` = "don't touch", outer /// is intentional: outer `None` = "don't touch", outer
@ -154,7 +127,7 @@ pub struct NewSchedule {
/// "missing key" vs "explicit null" — the dashboard surface /// "missing key" vs "explicit null" — the dashboard surface
/// preserves the distinction. /// preserves the distinction.
/// ///
/// Target add/remove (#474 fast-follow): `targets_add` / `targets_remove` /// Target add/remove: `targets_add` / `targets_remove`
/// run inside the same transaction as the scalar field updates so /// run inside the same transaction as the scalar field updates so
/// "save my changes" is atomic. Remove delegates to the same /// "save my changes" is atomic. Remove delegates to the same
/// cancel-targets path used by `cancel_targets` (tombstoning, preserves /// cancel-targets path used by `cancel_targets` (tombstoning, preserves
@ -365,7 +338,7 @@ impl ScheduledPrompts {
Ok(skipped) Ok(skipped)
} }
/// Partial-update an existing schedule's mutable fields (#474). /// Partial-update an existing schedule's mutable fields.
/// Every scalar field is `Option<_>`; `None` means "leave the /// Every scalar field is `Option<_>`; `None` means "leave the
/// existing value alone", `Some(_)` means "set it to this." /// existing value alone", `Some(_)` means "set it to this."
/// Refuses cancelled rows (no point editing a tombstone — /// Refuses cancelled rows (no point editing a tombstone —
@ -373,8 +346,8 @@ impl ScheduledPrompts {
/// `interval_seconds = Some(0)` — same rule as submit-time /// `interval_seconds = Some(0)` — same rule as submit-time
/// validation. /// validation.
/// ///
/// Targets are mutable via `targets_remove` + `targets_add` /// Targets are mutable via `targets_remove` + `targets_add`.
/// (#478 fast-follow). Both are processed in the same /// Both are processed in the same
/// transaction, with **removes before adds** so a single PATCH /// transaction, with **removes before adds** so a single PATCH
/// can swap a target without ever leaving the schedule /// can swap a target without ever leaving the schedule
/// target-less mid-tx. Remove writes a tombstone via /// target-less mid-tx. Remove writes a tombstone via