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

@ -5,7 +5,7 @@
use std::sync::Arc;
use anyhow::{Result, bail};
use anyhow::{Context as _, Result, bail};
use hive_sh4re::{ApprovalKind, ApprovalStatus, HelperEvent, MANAGER_AGENT};
use crate::coordinator::{Coordinator, TransientKind};
@ -90,6 +90,15 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
coord.emit_rebuild_queue_snapshot();
Ok(())
}
ApprovalKind::SchedulePrompt => {
// No queue card for SchedulePrompt — the work is a single
// sqlite insert, the actual "running" lifetime lives on
// the scheduled-prompts surface itself (worker fires it
// at the scheduled time). Run inline + fire
// `ApprovalResolved` so the approval row leaves Pending
// immediately.
run_approval_schedule_prompt(&coord, approval).await
}
}
}
@ -127,6 +136,39 @@ pub async fn run_approval_apply_commit(
finish_approval(coord, &approval, result, terminal_tag, is_first_spawn)
}
/// Inline (non-queued) handler for `ApprovalKind::SchedulePrompt`.
/// On approve, decode the `SchedulePromptPayload` JSON from the
/// approval's `commit_ref`, insert a row into `scheduled_prompts`
/// (with `source = Approval { id }`), and fire `ApprovalResolved`.
/// The worker takes over from here — fan-out at fire time.
async fn run_approval_schedule_prompt(
coord: &Coordinator,
approval: hive_sh4re::Approval,
) -> Result<()> {
let result: Result<()> = async {
let payload: hive_sh4re::SchedulePromptPayload =
serde_json::from_str(&approval.commit_ref)
.context("decode SchedulePromptPayload from approval.commit_ref")?;
coord
.scheduled_prompts
.submit(crate::scheduled_prompts::NewSchedule {
owner: approval.agent.clone(),
targets: payload.targets,
body: payload.body,
first_fire_at_unix: payload.first_fire_at_unix,
interval_seconds: payload.interval_seconds,
description: payload.description,
source: crate::scheduled_prompts::ScheduleSource::Approval {
id: approval.id,
},
})
.map(|_| ())
.context("insert scheduled prompt")
}
.await;
finish_approval(coord, &approval, result, None, false)
}
/// Worker entry point for `ApprovalKind::UpdateMetaInputs` queue
/// entries. Inputs come from the approval row's `commit_ref` field
/// (JSON-encoded by the manager submit path), not the queue entry's
@ -301,6 +343,7 @@ fn finish_approval(
ApprovalKind::ApplyCommit => "apply_commit",
ApprovalKind::InitConfig => "init_config",
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
ApprovalKind::SchedulePrompt => "schedule_prompt",
};
let sha_short = approval
.fetched_sha
@ -350,9 +393,9 @@ fn finish_approval(
sha: approval.fetched_sha.clone(),
tag: terminal_tag,
}),
// UpdateMetaInputs: ApprovalResolved already carries the result.
// No separate lifecycle event needed.
ApprovalKind::UpdateMetaInputs => {}
// UpdateMetaInputs / SchedulePrompt: ApprovalResolved already
// carries the result. No separate lifecycle event needed.
ApprovalKind::UpdateMetaInputs | ApprovalKind::SchedulePrompt => {}
}
result
}
@ -684,6 +727,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()
ApprovalKind::ApplyCommit => "apply_commit",
ApprovalKind::InitConfig => "init_config",
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
ApprovalKind::SchedulePrompt => "schedule_prompt",
};
let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned());
let description = a.description.clone();