c0re: schedule_prompt approval kind + worker + manager surface (#444 step 2)

This commit is contained in:
damocles 2026-05-26 01:11:36 +02:00 committed by Mara
commit aa7d8d9c9a
9 changed files with 612 additions and 4 deletions

View file

@ -110,6 +110,14 @@ pub enum ApprovalKind {
/// JSON-encoded inputs array (`"[]"` = all inputs). Agent field is
/// set to `hm1nd` (the requesting manager).
UpdateMetaInputs,
/// Add a scheduled prompt (closes #444). On approval hive-c0re
/// inserts a row into `scheduled_prompts` with
/// `source = Approval { id }`; the worker fans the body out as
/// inbox messages to each target at the scheduled time, recurring
/// when `interval_seconds` is set. `commit_ref` stores the
/// JSON-encoded `SchedulePromptPayload` so the approval row carries
/// the full submission verbatim (target list, body, schedule).
SchedulePrompt,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -910,6 +918,93 @@ pub enum ManagerRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// Queue an approval to add a scheduled prompt (#444). The
/// requester (caller of this request) is recorded as the schedule
/// owner; on operator approval hive-c0re inserts the schedule and
/// the worker fans the body out at fire time. Even agent-self
/// schedules go through approval — the existing `remind` MCP tool
/// is the unapproved self-wake path.
RequestSchedulePrompt(SchedulePromptPayload),
/// Cancel a scheduled prompt (#444). `targets = None` cancels the
/// whole schedule; `Some(list)` cancels just those recipients,
/// auto-cancelling the parent when no active targets remain.
/// Authorization: manager can cancel its own schedules + any
/// sub-agent schedules (i.e. owner reachable via topology); the
/// operator surface bypasses this check.
CancelSchedule {
id: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
targets: Option<Vec<String>>,
},
/// List every schedule in the queue. Manager-side this is
/// unfiltered — the dashboard does the topology-filter for the
/// per-agent view.
ListSchedules,
}
/// Submission payload for `RequestSchedulePrompt`. Lives outside the
/// enum so it can also serialize into the approval row's `commit_ref`
/// (the dispatcher re-parses it on approve and inserts the schedule).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SchedulePromptPayload {
/// Names of recipient agents. Operator + `hm1nd` allowed.
pub targets: Vec<String>,
/// Message body delivered to each target's inbox at fire time.
/// Same size budget as `Send.body` — soft cap at the broker level.
pub body: String,
/// Absolute unix timestamp (seconds) for the FIRST fire. For
/// recurring schedules the worker then re-arms in
/// `interval_seconds` steps.
pub first_fire_at_unix: i64,
/// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_seconds: Option<u64>,
/// Optional description shown on the dashboard approval card AND
/// stored on the resulting schedule row for the operator's
/// "what is this?" reference later.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// Schedule row shape on the wire — mirror of
/// `scheduled_prompts::Schedule` but in the public crate so dashboard
/// + agent surfaces can deserialize without depending on
/// hive-c0re-internal types. Kept structurally identical to the
/// in-process type; the conversion is field-by-field in
/// `manager_server` / `dashboard`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireSchedule {
pub id: i64,
pub owner: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_seconds: Option<u64>,
pub next_fire_at_unix: i64,
pub created_at_unix: i64,
pub source: WireScheduleSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancelled_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub targets: Vec<WireScheduleTarget>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WireScheduleSource {
Operator,
Approval { id: i64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireScheduleTarget {
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancelled_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_fired_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_result: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -944,6 +1039,12 @@ pub enum ManagerResponse {
Logs {
content: String,
},
/// `ListSchedules` result (#444). Snapshot of every schedule
/// (active + cancelled-but-not-yet-reaped); the dashboard does the
/// per-agent topology filter on top.
Schedules {
schedules: Vec<WireSchedule>,
},
/// `GetLooseEnds` result: hive-wide loose ends (approvals +
/// unanswered questions). Same `LooseEnd` variants as the
/// agent surface; the manager's view is unfiltered.