refactor(#1474): replace too_many_arguments allows on pub fns with param structs
This commit is contained in:
parent
7c9954ceec
commit
3130e56cfb
9 changed files with 407 additions and 324 deletions
|
|
@ -533,17 +533,17 @@ async fn handle_turn<S: Surface>(
|
|||
let ended_at = serve_common::now_unix();
|
||||
let duration_ms = i64::try_from(started_instant.elapsed().as_millis()).unwrap_or(i64::MAX);
|
||||
let (open_threads, open_reminders) = S::post_turn_counts(socket).await;
|
||||
let row = serve_common::build_row(
|
||||
let row = serve_common::build_row(serve_common::TurnRowArgs {
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model_at_start,
|
||||
from.clone(),
|
||||
&outcome,
|
||||
model: model_at_start,
|
||||
wake_from: from.clone(),
|
||||
outcome: &outcome,
|
||||
bus,
|
||||
open_threads,
|
||||
open_reminders,
|
||||
);
|
||||
open_threads_count: open_threads,
|
||||
open_reminders_count: open_reminders,
|
||||
});
|
||||
stats.record(&row);
|
||||
}
|
||||
let pending = S::inbox_unread(socket).await;
|
||||
|
|
|
|||
|
|
@ -36,26 +36,36 @@ pub fn now_unix() -> i64 {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Field-named args for [`build_row`]. Mirrors the turn-stats row
|
||||
/// columns; `outcome` and `bus` borrow for the duration of the call.
|
||||
pub struct TurnRowArgs<'a> {
|
||||
pub started_at: i64,
|
||||
pub ended_at: i64,
|
||||
pub duration_ms: i64,
|
||||
pub model: String,
|
||||
pub wake_from: String,
|
||||
pub outcome: &'a TurnOutcome,
|
||||
pub bus: &'a Bus,
|
||||
pub open_threads_count: Option<u64>,
|
||||
pub open_reminders_count: Option<u64>,
|
||||
}
|
||||
|
||||
/// Assemble a `TurnStatRow` from the harness's per-turn state. Used by both
|
||||
/// the agent and manager serve loops — the shape is identical, only the
|
||||
/// post-turn count fetch helpers differ (and those stay in each binary).
|
||||
#[must_use]
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the turn-stats row columns 1:1; a builder struct used \
|
||||
only here would just relabel the same fields"
|
||||
)]
|
||||
pub fn build_row(
|
||||
started_at: i64,
|
||||
ended_at: i64,
|
||||
duration_ms: i64,
|
||||
model: String,
|
||||
wake_from: String,
|
||||
outcome: &TurnOutcome,
|
||||
bus: &Bus,
|
||||
open_threads_count: Option<u64>,
|
||||
open_reminders_count: Option<u64>,
|
||||
) -> TurnStatRow {
|
||||
pub fn build_row(args: TurnRowArgs<'_>) -> TurnStatRow {
|
||||
let TurnRowArgs {
|
||||
started_at,
|
||||
ended_at,
|
||||
duration_ms,
|
||||
model,
|
||||
wake_from,
|
||||
outcome,
|
||||
bus,
|
||||
open_threads_count,
|
||||
open_reminders_count,
|
||||
} = args;
|
||||
// Prefer the API-resolved model id (e.g. `claude-opus-4-8`) captured
|
||||
// from this turn's assistant events over the requested `--model`
|
||||
// name/alias, so the model-mix + cost rollup label the concrete
|
||||
|
|
|
|||
|
|
@ -46,17 +46,19 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
|||
run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await
|
||||
}
|
||||
ApprovalKind::ApplyCommit => {
|
||||
coord.rebuild_queue.enqueue_full(
|
||||
crate::rebuild_queue::QueueKind::Rebuild,
|
||||
approval.agent.clone(),
|
||||
crate::rebuild_queue::QueueSource::Approval,
|
||||
format!("approval #{id} apply commit"),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(id),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
coord
|
||||
.rebuild_queue
|
||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
||||
kind: crate::rebuild_queue::QueueKind::Rebuild,
|
||||
agent: approval.agent.clone(),
|
||||
source: crate::rebuild_queue::QueueSource::Approval,
|
||||
reason: format!("approval #{id} apply commit"),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(id),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -66,17 +68,19 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
|||
// dashboard can show *which* inputs are about to bump.
|
||||
let inputs: Vec<String> =
|
||||
serde_json::from_str(&approval.commit_ref).unwrap_or_default();
|
||||
let parent_id = coord.rebuild_queue.enqueue_full(
|
||||
crate::rebuild_queue::QueueKind::MetaUpdate,
|
||||
approval.agent.clone(),
|
||||
crate::rebuild_queue::QueueSource::Approval,
|
||||
format!("approval #{id} meta input update"),
|
||||
None,
|
||||
inputs.clone(),
|
||||
Some(id),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
let parent_id = coord
|
||||
.rebuild_queue
|
||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
||||
kind: crate::rebuild_queue::QueueKind::MetaUpdate,
|
||||
agent: approval.agent.clone(),
|
||||
source: crate::rebuild_queue::QueueSource::Approval,
|
||||
reason: format!("approval #{id} meta input update"),
|
||||
parent_id: None,
|
||||
inputs: inputs.clone(),
|
||||
approval_id: Some(id),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
// Pre-enqueue cascade rebuilds in topological order so
|
||||
// agents depending on updated inputs are rebuilt after the
|
||||
// lock bump, matching the dashboard post_meta_update path.
|
||||
|
|
@ -95,17 +99,19 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
ApprovalKind::Spawn => {
|
||||
coord.rebuild_queue.enqueue_full(
|
||||
crate::rebuild_queue::QueueKind::Spawn,
|
||||
approval.agent.clone(),
|
||||
crate::rebuild_queue::QueueSource::Approval,
|
||||
format!("approval #{id} spawn"),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(id),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
coord
|
||||
.rebuild_queue
|
||||
.enqueue_full(crate::rebuild_queue::FullEnqueue {
|
||||
kind: crate::rebuild_queue::QueueKind::Spawn,
|
||||
agent: approval.agent.clone(),
|
||||
source: crate::rebuild_queue::QueueSource::Approval,
|
||||
reason: format!("approval #{id} spawn"),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(id),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -363,15 +369,15 @@ fn finish_approval(
|
|||
.as_deref()
|
||||
.map(|s| s[..s.len().min(12)].to_owned());
|
||||
let status_str = if ok { "approved" } else { "failed" };
|
||||
coord.emit_approval_resolved(
|
||||
approval.id,
|
||||
&approval.agent,
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id: approval.id,
|
||||
agent: &approval.agent,
|
||||
approval_kind,
|
||||
sha_short,
|
||||
status_str,
|
||||
note.clone(),
|
||||
approval.description.clone(),
|
||||
);
|
||||
status: status_str,
|
||||
note: note.clone(),
|
||||
description: approval.description.clone(),
|
||||
});
|
||||
// For spawn/rebuild/init_config approvals, also surface the underlying
|
||||
// action so the manager knows whether the lifecycle step succeeded.
|
||||
// The ApprovalResolved event already carries the same `ok` signal but
|
||||
|
|
@ -756,15 +762,15 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()
|
|||
sha,
|
||||
tag,
|
||||
});
|
||||
coord.emit_approval_resolved(
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id,
|
||||
&agent_owned,
|
||||
agent: &agent_owned,
|
||||
approval_kind,
|
||||
sha_short,
|
||||
"denied",
|
||||
note.map(String::from),
|
||||
status: "denied",
|
||||
note: note.map(String::from),
|
||||
description,
|
||||
);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,7 +298,19 @@ pub(crate) async fn dispatch_shared(
|
|||
since,
|
||||
until,
|
||||
} => {
|
||||
dispatch_host_journal(agent, unit, container, lines, priority, grep, since, until).await
|
||||
dispatch_host_journal(
|
||||
agent,
|
||||
HostJournalArgs {
|
||||
unit,
|
||||
container,
|
||||
lines,
|
||||
priority,
|
||||
grep,
|
||||
since,
|
||||
until,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
// Not a shared variant.
|
||||
_ => return None,
|
||||
|
|
@ -561,21 +573,28 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
///
|
||||
/// The manager is not exempt - grant `read_host_journal` in
|
||||
/// `meta/capabilities.json` to enable it for any agent including the manager.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the GetHostJournal wire variant 1:1 at this single \
|
||||
call site; a params struct would just relabel the same fields"
|
||||
)]
|
||||
pub async fn dispatch_host_journal(
|
||||
agent: &str,
|
||||
unit: &Option<String>,
|
||||
container: &Option<String>,
|
||||
lines: &Option<u32>,
|
||||
priority: &Option<hive_sh4re::JournalPriority>,
|
||||
grep: &Option<String>,
|
||||
since: &Option<String>,
|
||||
until: &Option<String>,
|
||||
) -> AgentResponse {
|
||||
/// Field-named journal-query knobs for [`dispatch_host_journal`].
|
||||
/// Borrows straight from the matched `GetHostJournal` request variant.
|
||||
pub struct HostJournalArgs<'a> {
|
||||
pub unit: &'a Option<String>,
|
||||
pub container: &'a Option<String>,
|
||||
pub lines: &'a Option<u32>,
|
||||
pub priority: &'a Option<hive_sh4re::JournalPriority>,
|
||||
pub grep: &'a Option<String>,
|
||||
pub since: &'a Option<String>,
|
||||
pub until: &'a Option<String>,
|
||||
}
|
||||
|
||||
pub async fn dispatch_host_journal(agent: &str, args: HostJournalArgs<'_>) -> AgentResponse {
|
||||
let HostJournalArgs {
|
||||
unit,
|
||||
container,
|
||||
lines,
|
||||
priority,
|
||||
grep,
|
||||
since,
|
||||
until,
|
||||
} = args;
|
||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::ReadHostJournal) {
|
||||
return AgentResponse::Err {
|
||||
message: "agent does not have the read_host_journal capability".to_owned(),
|
||||
|
|
|
|||
|
|
@ -349,6 +349,33 @@ impl TransientKind {
|
|||
}
|
||||
}
|
||||
|
||||
/// Field-named payload for [`Coordinator::emit_approval_resolved`].
|
||||
/// Mirrors the `ApprovalResolved` dashboard-event fields. `agent`
|
||||
/// borrows from the caller; `approval_kind` / `status` are
|
||||
/// compile-time constants.
|
||||
pub struct ApprovalResolved<'a> {
|
||||
pub id: i64,
|
||||
pub agent: &'a str,
|
||||
pub approval_kind: &'static str,
|
||||
pub sha_short: Option<String>,
|
||||
pub status: &'static str,
|
||||
pub note: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Field-named payload for [`Coordinator::emit_question_added`].
|
||||
/// Mirrors the `QuestionAdded` dashboard-event fields; all references
|
||||
/// share the caller's lifetime.
|
||||
pub struct QuestionAdded<'a> {
|
||||
pub id: i64,
|
||||
pub asker: &'a str,
|
||||
pub question: &'a str,
|
||||
pub options: &'a [String],
|
||||
pub multi: bool,
|
||||
pub deadline_at: Option<i64>,
|
||||
pub target: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Coordinator {
|
||||
pub fn open(
|
||||
db_path: &Path,
|
||||
|
|
@ -652,21 +679,19 @@ impl Coordinator {
|
|||
/// already have an authoritative timestamp from the db update,
|
||||
/// the tiny skew between "row updated" and "event emitted" is
|
||||
/// presentation-only and doesn't matter to clients.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the approval-resolved event payload fields; \
|
||||
bundling them into a struct used only here adds no clarity"
|
||||
)]
|
||||
pub fn emit_approval_resolved(
|
||||
&self,
|
||||
id: i64,
|
||||
agent: &str,
|
||||
approval_kind: &'static str,
|
||||
sha_short: Option<String>,
|
||||
status: &'static str,
|
||||
note: Option<String>,
|
||||
description: Option<String>,
|
||||
) {
|
||||
///
|
||||
/// Takes [`ApprovalResolved`] rather than a positional arg list so
|
||||
/// the seven fields are named at every call site.
|
||||
pub fn emit_approval_resolved(&self, ev: ApprovalResolved<'_>) {
|
||||
let ApprovalResolved {
|
||||
id,
|
||||
agent,
|
||||
approval_kind,
|
||||
sha_short,
|
||||
status,
|
||||
note,
|
||||
description,
|
||||
} = ev;
|
||||
let resolved_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()
|
||||
|
|
@ -689,21 +714,16 @@ impl Coordinator {
|
|||
/// both operator-targeted (`target = None`) and peer-to-peer
|
||||
/// (`target = Some(agent)`) threads — the dashboard surfaces
|
||||
/// both, distinguishing visually + offering operator override.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the question-added event payload fields; \
|
||||
bundling them into a struct used only here adds no clarity"
|
||||
)]
|
||||
pub fn emit_question_added(
|
||||
&self,
|
||||
id: i64,
|
||||
asker: &str,
|
||||
question: &str,
|
||||
options: &[String],
|
||||
multi: bool,
|
||||
deadline_at: Option<i64>,
|
||||
target: Option<&str>,
|
||||
) {
|
||||
pub fn emit_question_added(&self, ev: &QuestionAdded<'_>) {
|
||||
let &QuestionAdded {
|
||||
id,
|
||||
asker,
|
||||
question,
|
||||
options,
|
||||
multi,
|
||||
deadline_at,
|
||||
target,
|
||||
} = ev;
|
||||
let asked_at = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()
|
||||
|
|
|
|||
|
|
@ -82,15 +82,15 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
|
|||
.fetched_sha
|
||||
.as_deref()
|
||||
.map(|s| s[..s.len().min(12)].to_owned());
|
||||
coord.emit_approval_resolved(
|
||||
a.id,
|
||||
&a.agent,
|
||||
"apply_commit",
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id: a.id,
|
||||
agent: &a.agent,
|
||||
approval_kind: "apply_commit",
|
||||
sha_short,
|
||||
"failed",
|
||||
Some(note.to_owned()),
|
||||
a.description.clone(),
|
||||
);
|
||||
status: "failed",
|
||||
note: Some(note.to_owned()),
|
||||
description: a.description.clone(),
|
||||
});
|
||||
false
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -210,12 +210,14 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
coord,
|
||||
MANAGER_AGENT,
|
||||
*id,
|
||||
body.clone(),
|
||||
description.clone(),
|
||||
*interval_seconds,
|
||||
*next_fire_at_unix,
|
||||
targets_add.clone(),
|
||||
targets_remove.clone(),
|
||||
EditSchedulePatch {
|
||||
body: body.clone(),
|
||||
description: description.clone(),
|
||||
interval_seconds: *interval_seconds,
|
||||
next_fire_at_unix: *next_fire_at_unix,
|
||||
targets_add: targets_add.clone(),
|
||||
targets_remove: targets_remove.clone(),
|
||||
},
|
||||
),
|
||||
ManagerRequest::ListSchedules => match coord.scheduled_prompts.list() {
|
||||
Ok(schedules) => ManagerResponse::Schedules {
|
||||
|
|
@ -425,15 +427,15 @@ pub(crate) async fn submit_apply_commit(
|
|||
// explanation of why the approval can't be approved.
|
||||
let note = format!("{e:#}");
|
||||
let _ = coord.approvals.mark_failed(id, ¬e);
|
||||
coord.emit_approval_resolved(
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id,
|
||||
agent,
|
||||
"apply_commit",
|
||||
None,
|
||||
"failed",
|
||||
Some(note),
|
||||
description.map(str::to_owned),
|
||||
);
|
||||
approval_kind: "apply_commit",
|
||||
sha_short: None,
|
||||
status: "failed",
|
||||
note: Some(note),
|
||||
description: description.map(str::to_owned),
|
||||
});
|
||||
return Err(anyhow::anyhow!("git_fetch_to_tag: {e:#}"));
|
||||
}
|
||||
};
|
||||
|
|
@ -455,29 +457,29 @@ pub(crate) async fn submit_apply_commit(
|
|||
if let Err(e) = crate::flake_check::check_lock_in_sync(&applied_dir, &tag, id).await {
|
||||
let note = format!("{e:#}");
|
||||
let _ = coord.approvals.mark_failed(id, ¬e);
|
||||
coord.emit_approval_resolved(
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id,
|
||||
agent,
|
||||
"apply_commit",
|
||||
Some(sha_short.clone()),
|
||||
"failed",
|
||||
Some(note),
|
||||
description.map(str::to_owned),
|
||||
);
|
||||
approval_kind: "apply_commit",
|
||||
sha_short: Some(sha_short.clone()),
|
||||
status: "failed",
|
||||
note: Some(note),
|
||||
description: description.map(str::to_owned),
|
||||
});
|
||||
return Err(anyhow::anyhow!("flake lock-sync check: {e:#}"));
|
||||
}
|
||||
if let Err(e) = crate::flake_check::check_no_duplicate_inputs(&applied_dir, &tag).await {
|
||||
let note = format!("{e:#}");
|
||||
let _ = coord.approvals.mark_failed(id, ¬e);
|
||||
coord.emit_approval_resolved(
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id,
|
||||
agent,
|
||||
"apply_commit",
|
||||
Some(sha_short.clone()),
|
||||
"failed",
|
||||
Some(note),
|
||||
description.map(str::to_owned),
|
||||
);
|
||||
approval_kind: "apply_commit",
|
||||
sha_short: Some(sha_short.clone()),
|
||||
status: "failed",
|
||||
note: Some(note),
|
||||
description: description.map(str::to_owned),
|
||||
});
|
||||
return Err(anyhow::anyhow!("flake dedup check: {e:#}"));
|
||||
}
|
||||
// Mirror the freshly-planted proposal/<id> tag to the forge.
|
||||
|
|
@ -660,6 +662,24 @@ async fn handle_fire_schedule_now(
|
|||
}
|
||||
}
|
||||
|
||||
/// Field-named PATCH payload for [`handle_edit_schedule`]. Every
|
||||
/// field is "leave alone" when `None`; the double-`Option` fields
|
||||
/// additionally distinguish clear (`Some(None)`) from set
|
||||
/// (`Some(Some(v))`).
|
||||
#[allow(
|
||||
clippy::option_option,
|
||||
reason = "double-Option carries three-state PATCH semantics: outer None = \
|
||||
leave alone, Some(None) = clear, Some(Some(v)) = set"
|
||||
)]
|
||||
struct EditSchedulePatch {
|
||||
body: Option<String>,
|
||||
description: Option<Option<String>>,
|
||||
interval_seconds: Option<Option<u64>>,
|
||||
next_fire_at_unix: Option<i64>,
|
||||
targets_add: Option<Vec<String>>,
|
||||
targets_remove: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Authorize + dispatch a `EditSchedule` patch. Same ownership
|
||||
/// rules as `CancelSchedule` — the manager can edit
|
||||
/// schedules it owns + any owned by an agent in its subtree.
|
||||
|
|
@ -668,27 +688,20 @@ async fn handle_fire_schedule_now(
|
|||
/// zero-interval validation. Returns `Ok` on a clean update;
|
||||
/// `Err` with the underlying message on any auth / validation
|
||||
/// failure so the dashboard can surface it verbatim.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the edit-schedule PATCH fields 1:1; bundling them into \
|
||||
a struct used only here adds no clarity"
|
||||
)]
|
||||
#[allow(
|
||||
clippy::option_option,
|
||||
reason = "double-Option carries three-state PATCH semantics: outer None = \
|
||||
leave alone, Some(None) = clear, Some(Some(v)) = set"
|
||||
)]
|
||||
fn handle_edit_schedule(
|
||||
coord: &Arc<Coordinator>,
|
||||
requester: &str,
|
||||
schedule_id: i64,
|
||||
body: Option<String>,
|
||||
description: Option<Option<String>>,
|
||||
interval_seconds: Option<Option<u64>>,
|
||||
next_fire_at_unix: Option<i64>,
|
||||
targets_add: Option<Vec<String>>,
|
||||
targets_remove: Option<Vec<String>>,
|
||||
patch: EditSchedulePatch,
|
||||
) -> ManagerResponse {
|
||||
let EditSchedulePatch {
|
||||
body,
|
||||
description,
|
||||
interval_seconds,
|
||||
next_fire_at_unix,
|
||||
targets_add,
|
||||
targets_remove,
|
||||
} = patch;
|
||||
let schedule = match coord.scheduled_prompts.get(schedule_id) {
|
||||
Ok(Some(s)) => s,
|
||||
Ok(None) => {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,15 @@ pub fn handle_ask(
|
|||
}
|
||||
// Always fire on the dashboard channel — both operator-targeted
|
||||
// and peer threads now surface in the dashboard's questions pane.
|
||||
coord.emit_question_added(id, asker, question, options, multi, deadline_at, target);
|
||||
coord.emit_question_added(&crate::coordinator::QuestionAdded {
|
||||
id,
|
||||
asker,
|
||||
question,
|
||||
options,
|
||||
multi,
|
||||
deadline_at,
|
||||
target,
|
||||
});
|
||||
if let Some(t) = ttl {
|
||||
spawn_question_watchdog(coord, id, t);
|
||||
}
|
||||
|
|
@ -195,15 +203,15 @@ pub fn handle_cancel_loose_end(
|
|||
.fetched_sha
|
||||
.as_deref()
|
||||
.map(|s| s[..s.len().min(12)].to_owned());
|
||||
coord.emit_approval_resolved(
|
||||
approval.id,
|
||||
&approval.agent,
|
||||
kind_to_str(approval.kind),
|
||||
coord.emit_approval_resolved(crate::coordinator::ApprovalResolved {
|
||||
id: approval.id,
|
||||
agent: &approval.agent,
|
||||
approval_kind: kind_to_str(approval.kind),
|
||||
sha_short,
|
||||
"cancelled",
|
||||
approval.note,
|
||||
approval.description,
|
||||
);
|
||||
status: "cancelled",
|
||||
note: approval.note,
|
||||
description: approval.description,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,6 +259,22 @@ impl Default for RebuildQueue {
|
|||
}
|
||||
}
|
||||
|
||||
/// Full-shape submit spec for [`RebuildQueue::enqueue_full`] — every
|
||||
/// `QueueEntry` field settable at submit time. The thinner `enqueue`
|
||||
/// / `enqueue_with_inputs` / `enqueue_with_perm` wrappers build this
|
||||
/// for the common cases.
|
||||
pub struct FullEnqueue {
|
||||
pub kind: QueueKind,
|
||||
pub agent: String,
|
||||
pub source: QueueSource,
|
||||
pub reason: String,
|
||||
pub parent_id: Option<u64>,
|
||||
pub inputs: Vec<String>,
|
||||
pub approval_id: Option<i64>,
|
||||
pub perm_payload: Option<PermPayload>,
|
||||
pub depends_on: Vec<u64>,
|
||||
}
|
||||
|
||||
impl RebuildQueue {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
|
|
@ -294,17 +310,17 @@ impl RebuildQueue {
|
|||
reason: String,
|
||||
parent_id: Option<u64>,
|
||||
) -> u64 {
|
||||
self.enqueue_full(
|
||||
self.enqueue_full(FullEnqueue {
|
||||
kind,
|
||||
agent,
|
||||
source,
|
||||
reason,
|
||||
parent_id,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Same as `enqueue` but carries an `inputs` payload — used by
|
||||
|
|
@ -321,17 +337,17 @@ impl RebuildQueue {
|
|||
parent_id: Option<u64>,
|
||||
inputs: Vec<String>,
|
||||
) -> u64 {
|
||||
self.enqueue_full(
|
||||
self.enqueue_full(FullEnqueue {
|
||||
kind,
|
||||
agent,
|
||||
source,
|
||||
reason,
|
||||
parent_id,
|
||||
inputs,
|
||||
None,
|
||||
None,
|
||||
Vec::new(),
|
||||
)
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Enqueue a `PermChange` entry for `agent`. The worker applies the
|
||||
|
|
@ -344,17 +360,17 @@ impl RebuildQueue {
|
|||
reason: String,
|
||||
payload: PermPayload,
|
||||
) -> u64 {
|
||||
self.enqueue_full(
|
||||
QueueKind::PermChange,
|
||||
self.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::PermChange,
|
||||
agent,
|
||||
source,
|
||||
reason,
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
Some(payload),
|
||||
Vec::new(),
|
||||
)
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: Some(payload),
|
||||
depends_on: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Full-shape enqueue — every `QueueEntry` field that's settable
|
||||
|
|
@ -362,27 +378,18 @@ impl RebuildQueue {
|
|||
/// `enqueue_with_perm` delegate to this; the approval-driven POST
|
||||
/// handlers call it directly with the source row's id so the
|
||||
/// worker can re-fetch the kind-specific payload.
|
||||
// 10 args: the queue entry has 6 independent submit-time fields plus
|
||||
// four kind-specific payload fields (inputs, approval_id, perm_payload,
|
||||
// depends_on). A builder struct would obscure the call sites; the
|
||||
// shorter wrappers already cover all common cases.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "args mirror the queue-entry fields the row is built from; the \
|
||||
thinner enqueue helpers wrap this for the common cases"
|
||||
)]
|
||||
pub fn enqueue_full(
|
||||
&self,
|
||||
kind: QueueKind,
|
||||
agent: String,
|
||||
source: QueueSource,
|
||||
reason: String,
|
||||
parent_id: Option<u64>,
|
||||
inputs: Vec<String>,
|
||||
approval_id: Option<i64>,
|
||||
perm_payload: Option<PermPayload>,
|
||||
depends_on: Vec<u64>,
|
||||
) -> u64 {
|
||||
pub fn enqueue_full(&self, spec: FullEnqueue) -> u64 {
|
||||
let FullEnqueue {
|
||||
kind,
|
||||
agent,
|
||||
source,
|
||||
reason,
|
||||
parent_id,
|
||||
inputs,
|
||||
approval_id,
|
||||
perm_payload,
|
||||
depends_on,
|
||||
} = spec;
|
||||
let mut inner = self.inner.lock().expect("rebuild_queue mutex poisoned");
|
||||
// Dedup against a pending entry with the same (kind, agent) —
|
||||
// and, for MetaUpdate, the same `inputs` list (see method
|
||||
|
|
@ -1213,17 +1220,17 @@ mod tests {
|
|||
#[test]
|
||||
fn approval_entries_keep_approval_id() {
|
||||
let q = RebuildQueue::new();
|
||||
let id = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"agent-a".to_owned(),
|
||||
QueueSource::Approval,
|
||||
"approval #42 apply commit".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(42),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
let id = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "agent-a".to_owned(),
|
||||
source: QueueSource::Approval,
|
||||
reason: "approval #42 apply commit".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(42),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
let snap = q.snapshot();
|
||||
let entry = snap.iter().find(|e| e.id == id).expect("entry present");
|
||||
assert_eq!(entry.approval_id, Some(42));
|
||||
|
|
@ -1237,43 +1244,43 @@ mod tests {
|
|||
// approve click is a separate piece of work even when the
|
||||
// (kind, agent) pair matches.
|
||||
let q = RebuildQueue::new();
|
||||
let a = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"agent-a".to_owned(),
|
||||
QueueSource::Approval,
|
||||
"approval #1".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
let b = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"agent-a".to_owned(),
|
||||
QueueSource::Approval,
|
||||
"approval #2".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(2),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
let a = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "agent-a".to_owned(),
|
||||
source: QueueSource::Approval,
|
||||
reason: "approval #1".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(1),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
let b = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "agent-a".to_owned(),
|
||||
source: QueueSource::Approval,
|
||||
reason: "approval #2".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(2),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
assert_ne!(a, b);
|
||||
assert_eq!(q.snapshot().len(), 2);
|
||||
// Same approval_id submitted twice DOES dedup (rapid double-
|
||||
// click on the dashboard's approve button is a single op).
|
||||
let c = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"agent-a".to_owned(),
|
||||
QueueSource::Approval,
|
||||
"approval #1 (duplicate)".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
Some(1),
|
||||
None,
|
||||
Vec::new(),
|
||||
);
|
||||
let c = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "agent-a".to_owned(),
|
||||
source: QueueSource::Approval,
|
||||
reason: "approval #1 (duplicate)".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: Some(1),
|
||||
perm_payload: None,
|
||||
depends_on: Vec::new(),
|
||||
});
|
||||
assert_eq!(a, c);
|
||||
assert_eq!(q.snapshot().len(), 2);
|
||||
}
|
||||
|
|
@ -1459,17 +1466,17 @@ mod tests {
|
|||
"first".to_owned(),
|
||||
None,
|
||||
);
|
||||
let b = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"agent-b".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"second (blocked on a)".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![a],
|
||||
);
|
||||
let b = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "agent-b".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "second (blocked on a)".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![a],
|
||||
});
|
||||
// B depends on A — take_next should give A first.
|
||||
let first = q.take_next().expect("a is ready");
|
||||
assert_eq!(first.id, a);
|
||||
|
|
@ -1529,17 +1536,17 @@ mod tests {
|
|||
"dep must be evicted from history"
|
||||
);
|
||||
// An entry that depends on the (evicted) dep must be immediately runnable.
|
||||
let downstream = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"downstream".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"downstream (dep evicted = resolved)".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![dep],
|
||||
);
|
||||
let downstream = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "downstream".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "downstream (dep evicted = resolved)".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![dep],
|
||||
});
|
||||
let got = q.take_next().expect("downstream runnable when dep evicted");
|
||||
assert_eq!(got.id, downstream);
|
||||
}
|
||||
|
|
@ -1563,42 +1570,42 @@ mod tests {
|
|||
"d2".to_owned(),
|
||||
None,
|
||||
);
|
||||
let a = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"target".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"r".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![dep1],
|
||||
);
|
||||
let a = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "target".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "r".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![dep1],
|
||||
});
|
||||
// Same kind+agent but different depends_on — must NOT dedup.
|
||||
let b = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"target".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"r".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![dep2],
|
||||
);
|
||||
let b = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "target".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "r".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![dep2],
|
||||
});
|
||||
assert_ne!(a, b, "different depends_on must produce distinct entries");
|
||||
// Same depends_on as a — must dedup.
|
||||
let c = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"target".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"r again".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![dep1],
|
||||
);
|
||||
let c = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "target".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "r again".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![dep1],
|
||||
});
|
||||
assert_eq!(a, c, "identical depends_on must dedup");
|
||||
}
|
||||
|
||||
|
|
@ -1615,17 +1622,17 @@ mod tests {
|
|||
"a".to_owned(),
|
||||
None,
|
||||
);
|
||||
let b = q.enqueue_full(
|
||||
QueueKind::Rebuild,
|
||||
"b".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"b (blocked on a)".to_owned(),
|
||||
None,
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
vec![a],
|
||||
);
|
||||
let b = q.enqueue_full(FullEnqueue {
|
||||
kind: QueueKind::Rebuild,
|
||||
agent: "b".to_owned(),
|
||||
source: QueueSource::Manual,
|
||||
reason: "b (blocked on a)".to_owned(),
|
||||
parent_id: None,
|
||||
inputs: Vec::new(),
|
||||
approval_id: None,
|
||||
perm_payload: None,
|
||||
depends_on: vec![a],
|
||||
});
|
||||
q.take_next(); // pop a, mark Running
|
||||
q.finish(a, QueueState::Failed, Some("nix build exploded".to_owned()));
|
||||
let got = q.take_next().expect("b runnable after a failed");
|
||||
|
|
|
|||
Loading…
Reference in a new issue