hive-c0re stops owning scheduled prompts; agents lose the schedule tools
Scheduled prompts are swarm-level now (swarmctl schedule, #4936). The hive keeps the rows it has only so the operator can copy them over. - hive-c0re no longer fires anything: the scheduled-prompts worker is gone, and with it every create/edit/cancel/pause/resume/fire path (dashboard endpoints, agent-socket verbs, the SchedulePrompt approval kind and its approve handler). The store keeps open + list; GET /api/schedules stays. - A v6 approvals migration drops schedule_prompt rows. - Nothing submits approvals any more, so the submit path goes too: Approvals::submit_kind (now a test fixture), the ApprovalAdded dashboard event and its emitter. - Coordinator::push_todo, whose only caller was the worker, goes, and with it hive_host_sock::agent_todo_socket and hive-c0re's hive-agent-sock / hive-sock-client deps. - The schedules_changed dashboard event goes; nothing mutates the rows. - Agents lose request_schedule_prompt, cancel_schedule, fire_schedule_now, edit_schedule and list_schedules, the `scheduling` tool group, their system-prompt line and their transcript formatters. `remind` is unchanged.
This commit is contained in:
parent
7d80f5146d
commit
c3a54bbdd0
31 changed files with 273 additions and 3124 deletions
|
|
@ -9,8 +9,8 @@ would hit. HTTP is the sole transport; there is no stdio mode here.
|
|||
## When to use it
|
||||
|
||||
This is where the core hyperhive tool surface lives: `send`, `recv`,
|
||||
`remind`, `get_loose_ends`, `set_status`,
|
||||
`get_agent_meta`, scheduling, and the approval-request tools. Reach
|
||||
`remind`, `get_loose_ends`, `set_status`
|
||||
and `get_agent_meta`. Reach
|
||||
for this crate when you're adding or changing a built-in tool rather
|
||||
than an `extraMcpServers` add-on — those are separate stdio bridges
|
||||
(see `hive-bash-mcp`, `hive-matrix-mcp`) that dial the harness socket
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ pub struct CompactArgs {
|
|||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Privileged tool arg types (lifecycle, approvals, scheduling)
|
||||
// Privileged tool arg types (lifecycle, approvals)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
|
|
@ -123,89 +123,3 @@ pub struct CancelLooseEndArgs {
|
|||
/// Row id from the matching `get_loose_ends` entry.
|
||||
pub id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct RequestSchedulePromptArgs {
|
||||
/// Recipient agents — one schedule fires to many inboxes at the
|
||||
/// scheduled time. `operator` is not a valid target — she isn't a
|
||||
/// container with a todo inbox to push a wake into, and this is
|
||||
/// for fanning a prompt out to agents, not scheduling something
|
||||
/// at her.
|
||||
pub targets: Vec<String>,
|
||||
/// Message body delivered to each target's inbox at fire time.
|
||||
/// Same size budget as `send` bodies.
|
||||
pub body: String,
|
||||
/// Absolute unix timestamp (seconds) for the FIRST fire. For
|
||||
/// recurring schedules the worker re-arms in
|
||||
/// `interval_seconds` steps from this point on.
|
||||
pub first_fire_at_unix: i64,
|
||||
/// `None` / absent = one-shot. `Some(n > 0)` = recurring every
|
||||
/// `n` seconds. The worker clamps catch-up so a long downtime
|
||||
/// fires ONCE on resume (skipped-cycle count surfaces in the
|
||||
/// per-target `last_result`), not N delayed pulses in a row.
|
||||
#[serde(default)]
|
||||
pub interval_seconds: Option<u64>,
|
||||
/// Optional description shown on the dashboard approval card +
|
||||
/// preserved on the schedule row for later operator reference.
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct FireScheduleNowArgs {
|
||||
/// Schedule id to fire out of band. Get this from a prior
|
||||
/// `list_schedules` call or the approval-resolved event for
|
||||
/// the originating `request_schedule_prompt`.
|
||||
pub id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct CancelScheduleArgs {
|
||||
/// Schedule id from a prior `list_schedules` call or the
|
||||
/// approval-resolved event for a `request_schedule_prompt`.
|
||||
pub id: i64,
|
||||
/// Optional target list. `None` / empty = cancel the entire
|
||||
/// schedule. `Some(["alice", "bob"])` = cancel just those
|
||||
/// recipients (the schedule keeps firing for any remaining
|
||||
/// active targets, and auto-cancels its parent row when every
|
||||
/// target is gone).
|
||||
#[serde(default)]
|
||||
pub targets: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct EditScheduleArgs {
|
||||
/// Schedule id from a prior `list_schedules` call or the
|
||||
/// approval-resolved event for a `request_schedule_prompt`.
|
||||
pub id: i64,
|
||||
/// New body text. Omit to keep the existing one.
|
||||
#[serde(default)]
|
||||
pub body: Option<String>,
|
||||
/// New description. Omit to keep the existing one. (To CLEAR
|
||||
/// the description, use the dashboard PATCH endpoint
|
||||
/// directly — the agent surface intentionally keeps the args
|
||||
/// flat / non-nullable to dodge the doubly-wrapped Option
|
||||
/// schemars quirk; clearing fields is rare and operator-side.)
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
/// Recurring interval in seconds. Omit to keep the existing
|
||||
/// cadence; pass an explicit value to set a new one. Toggling
|
||||
/// recurring↔one-shot (clearing the interval) is operator-only
|
||||
/// for the same reason as `description` above.
|
||||
#[serde(default)]
|
||||
pub interval_seconds: Option<u64>,
|
||||
/// New absolute unix timestamp for the next fire. Omit to
|
||||
/// leave the schedule on its current cadence.
|
||||
#[serde(default)]
|
||||
pub next_fire_at_unix: Option<i64>,
|
||||
/// Names of new targets to add. Replace-on-conflict: re-adding
|
||||
/// a previously cancelled target resets its history (operator
|
||||
/// intent on re-add = "this target is active again").
|
||||
#[serde(default)]
|
||||
pub targets_add: Option<Vec<String>>,
|
||||
/// Names of targets to cancel. Tombstones preserve per-target
|
||||
/// audit; when no active targets remain the schedule
|
||||
/// auto-cancels.
|
||||
#[serde(default)]
|
||||
pub targets_remove: Option<Vec<String>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ mod args;
|
|||
mod render;
|
||||
|
||||
pub use args::{
|
||||
AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, EditScheduleArgs,
|
||||
FireScheduleNowArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs,
|
||||
RequestSchedulePromptArgs, SendArgs, SetStatusArgs,
|
||||
AckUntilArgs, CancelLooseEndArgs, CompactArgs, GetAgentMetaArgs, MarkTodosDoneArgs, RecvArgs,
|
||||
RemindArgs, SendArgs, SetStatusArgs,
|
||||
};
|
||||
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
|
||||
|
||||
|
|
@ -519,159 +518,6 @@ impl AgentServer {
|
|||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Queue an approval to add a scheduled prompt — one body delivered to \
|
||||
N agent inboxes at a target time, optionally recurring every `interval_seconds`. \
|
||||
The operator approves; on approve hive-c0re inserts the schedule and the worker \
|
||||
fans it out. Even self-targeted schedules go through this flow (the operator pays \
|
||||
for the wake-up tokens); the existing `remind` MCP tool stays the quick \
|
||||
no-approval self-wake path. \n\n\
|
||||
Catch-up clamp: if hive-c0re is down across multiple intervals, only ONE delayed \
|
||||
fire happens on resume (per recurring schedule). The skipped-cycle count surfaces \
|
||||
in the per-target `last_result` for the operator's audit trail. \n\n\
|
||||
Per-target failure: a target name that doesn't resolve to a live agent → operator \
|
||||
gets a one-line advisory `Message` from `system`; the schedule keeps firing for \
|
||||
the other (live) targets."
|
||||
)]
|
||||
async fn request_schedule_prompt(
|
||||
&self,
|
||||
Parameters(args): Parameters<RequestSchedulePromptArgs>,
|
||||
) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("request_schedule_prompt", log, async move {
|
||||
let target_count = args.targets.len();
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_core_agent_sock::Request::RequestSchedulePrompt(
|
||||
hive_sh4re::manager::SchedulePromptPayload {
|
||||
targets: args.targets,
|
||||
body: args.body,
|
||||
first_fire_at_unix: args.first_fire_at_unix,
|
||||
interval_seconds: args.interval_seconds,
|
||||
description: args.description,
|
||||
},
|
||||
))
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(
|
||||
resp,
|
||||
"request_schedule_prompt",
|
||||
format!("approval queued: {target_count} target(s)"),
|
||||
),
|
||||
retries,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Fire a scheduled prompt out of band — runs the per-target fan-out \
|
||||
once immediately without disturbing the schedule's cadence. Recurring schedules \
|
||||
keep their next_fire_at unchanged (the manual fire is additive). One-shot \
|
||||
schedules are CONSUMED by the manual fire (cancelled afterwards): the operator's \
|
||||
intent on a one-shot is 'send this now, the scheduled time was wrong'. \n\n\
|
||||
Authorization mirrors `cancel_schedule`: you can fire any schedule, whoever owns it."
|
||||
)]
|
||||
async fn fire_schedule_now(&self, Parameters(args): Parameters<FireScheduleNowArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("fire_schedule_now", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_core_agent_sock::Request::FireScheduleNow { id })
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "fire_schedule_now", format!("fired #{id} now")),
|
||||
retries,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Cancel a scheduled prompt. With no `targets` field, cancels the \
|
||||
whole schedule (all recipients flipped). With a non-empty `targets` list, cancels \
|
||||
just those recipients; the schedule keeps firing for any remaining active targets \
|
||||
and auto-cancels its parent row when every target is cancelled. \n\n\
|
||||
Authorization: you can cancel any schedule, whoever owns it."
|
||||
)]
|
||||
async fn cancel_schedule(&self, Parameters(args): Parameters<CancelScheduleArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("cancel_schedule", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_core_agent_sock::Request::CancelSchedule {
|
||||
id: args.id,
|
||||
targets: args.targets,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "cancel_schedule", format!("cancelled #{id}")),
|
||||
retries,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Edit an existing scheduled prompt's mutable fields. Pass only \
|
||||
the fields you want to change — anything omitted keeps its current value. Editable: \
|
||||
`body`, `description`, `interval_seconds` (positive only via this tool; flipping \
|
||||
recurring→one-shot is operator-only via the dashboard), `next_fire_at_unix`, and \
|
||||
the target set via `targets_add` / `targets_remove`. Both target lists are \
|
||||
applied in the same transaction with removes-before-adds, so a single edit can \
|
||||
swap a target atomically. Re-adding a previously-removed target starts a fresh \
|
||||
per-target history (drops the tombstone). Draining all targets auto-cancels the \
|
||||
parent schedule. \n\n\
|
||||
Authorization mirrors `cancel_schedule` / `fire_schedule_now`: you can edit any \
|
||||
schedule, whoever owns it. \
|
||||
Refuses cancelled schedules (the row's terminal — submit a fresh one)."
|
||||
)]
|
||||
async fn edit_schedule(&self, Parameters(args): Parameters<EditScheduleArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
run_tool_envelope("edit_schedule", log, async move {
|
||||
let id = args.id;
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_core_agent_sock::Request::EditSchedule {
|
||||
id: args.id,
|
||||
body: args.body,
|
||||
description: args.description.map(Some),
|
||||
interval_seconds: args.interval_seconds.map(Some),
|
||||
next_fire_at_unix: args.next_fire_at_unix,
|
||||
targets_add: args.targets_add,
|
||||
targets_remove: args.targets_remove,
|
||||
})
|
||||
.await;
|
||||
annotate_retries(
|
||||
format_ack(resp, "edit_schedule", format!("edited #{id}")),
|
||||
retries,
|
||||
)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List every scheduled prompt in the hive, whoever owns it — active and \
|
||||
cancelled-but-not-yet-reaped. Returns schedule id, owner, body, target set with \
|
||||
per-target last_fired_at + last_result, next fire time, recurring interval. \
|
||||
Use this to look up an id before calling `cancel_schedule`, or to audit what \
|
||||
the hive is going to be woken up about next."
|
||||
)]
|
||||
async fn list_schedules(&self) -> String {
|
||||
run_tool_envelope("list_schedules", String::new(), async move {
|
||||
let (resp, retries) = self
|
||||
.dispatch(hive_core_agent_sock::Request::ListSchedules)
|
||||
.await;
|
||||
let body = match resp {
|
||||
Ok(hive_core_agent_sock::Response::Schedules { schedules }) => {
|
||||
serde_json::to_string(&schedules)
|
||||
.unwrap_or_else(|e| format!("list_schedules: serialise: {e:#}"))
|
||||
}
|
||||
other => reply_err(other, "list_schedules"),
|
||||
};
|
||||
annotate_retries(body, retries)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler(
|
||||
|
|
|
|||
|
|
@ -643,12 +643,12 @@ mod tests {
|
|||
Ok(hive_core_agent_sock::Response::OkWarn {
|
||||
warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()],
|
||||
}),
|
||||
"request_schedule_prompt",
|
||||
"schedule approval queued".to_owned(),
|
||||
"send",
|
||||
"sent to iris".to_owned(),
|
||||
);
|
||||
// The operation HAPPENED — dropping the success line would read as a
|
||||
// failure and invite a retry that queues a second approval.
|
||||
assert!(out.starts_with("schedule approval queued"));
|
||||
// failure and invite a retry that sends a second message.
|
||||
assert!(out.starts_with("sent to iris"));
|
||||
assert!(out.contains("⚠️ name is reserved"));
|
||||
assert!(out.contains("⚠️ second thing"));
|
||||
}
|
||||
|
|
@ -659,10 +659,10 @@ mod tests {
|
|||
// warning marker would pass the test above.
|
||||
let out = format_ack(
|
||||
Ok(hive_core_agent_sock::Response::Ok),
|
||||
"request_schedule_prompt",
|
||||
"queued".to_owned(),
|
||||
"send",
|
||||
"sent".to_owned(),
|
||||
);
|
||||
assert_eq!(out, "queued");
|
||||
assert_eq!(out, "sent");
|
||||
assert!(!out.contains('⚠'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue