zeroclaw-labs/zeroclaw · error · anyhow::Error
run {run_id} is parked at an approval gate; amend/revise app
Error message
run {run_id} is parked at an approval gate; amend/revise apply only to deterministic checkpoints — approve or deny instead What it means
The approval broker's resolve() was called with ApprovalDecision::Amend or ApprovalDecision::Revise while the run is parked at an approval gate. Amend/Revise are deterministic-checkpoint decisions (an editable piped draft, a predecessor step to re-run); an approval gate has neither, so the broker fails closed before any vote or ledger side effect.
Source
Thrown at crates/zeroclaw-runtime/src/sop/approval/broker.rs:481
.filter(|g| !g.is_empty())
&& !self
.resolver
.is_member(engine.approval_config(), &principal, group)
{
return Ok(BrokerOutcome::NotAuthorized {
required_group: group.to_string(),
});
}
match decision {
// A single authorized deny cancels the run (no quorum on denial - fail-safe).
ApprovalDecision::Deny { .. } => Ok(BrokerOutcome::Resolved(
engine.resolve_gate(run_id, decision, principal)?,
)),
// Amend/Revise are deterministic-checkpoint decisions: an approval
// gate has no piped draft to edit and no predecessor to re-run. Fail
// closed BEFORE any vote or ledger side effect.
ApprovalDecision::Amend { .. } | ApprovalDecision::Revise { .. } => anyhow::bail!(
"run {run_id} is parked at an approval gate; amend/revise apply only to \
deterministic checkpoints — approve or deny instead"
),
ApprovalDecision::Approve => {
// Unpoliced (no named policy) clears immediately - quorum-1 pass-through.
let Some((policy_name, cfg)) = policy.as_ref() else {
return Ok(BrokerOutcome::Resolved(
engine.resolve_gate(run_id, decision, principal)?,
));
};
let need = (cfg.quorum.max(1)) as usize;
if need <= 1 {
return Ok(BrokerOutcome::Resolved(
engine.resolve_gate(run_id, decision, principal)?,
));
}
// Refuse to record a quorum vote from a principal `approval_mode` would
// reject outright (the agent under OutOfBandRequired, an out-of-bandView on GitHub (pinned to 88bb9c8533)
Solutions
- Send ApprovalDecision::Approve or Deny for runs parked at approval gates.
- Gate the Amend/Revise UI on the park kind — only offer them for deterministic checkpoints.
- Inspect the run's park reason first to decide which decision variants are legal.
Example fix
// before
broker.resolve(run_id, ApprovalDecision::Amend { draft }).await?; // run is at an approval gate
// after
match run_park_kind(run_id) {
ParkKind::ApprovalGate => broker.resolve(run_id, ApprovalDecision::Approve).await?,
ParkKind::Checkpoint => broker.resolve(run_id, ApprovalDecision::Amend { draft }).await?,
} Defensive patterns
Strategy: validation
Validate before calling
fn legal_decisions(park: &ParkState) -> &'static [DecisionKind] {
match park {
ParkState::ApprovalGate => &[DecisionKind::Approve, DecisionKind::Deny],
ParkState::Checkpoint => &[DecisionKind::Approve, DecisionKind::Deny, DecisionKind::Amend, DecisionKind::Revise],
}
}
assert!(legal_decisions(&park).contains(&decision.kind())); Type guard
fn is_approval_gate(run: &RunView) -> bool {
matches!(run.park_reason(), Some(ParkReason::ApprovalGate { .. }))
} Try / catch
match broker.resolve(run_id, decision).await {
Err(e) if e.to_string().contains("amend/revise apply only to deterministic checkpoints") => {
eprintln!("run {run_id} is at an approval gate: resend as Approve or Deny");
}
rest => rest?,
} Prevention
- Derive the offered decision buttons from the run's park kind, not from a fixed menu.
- Document the decision matrix (gate: Approve/Deny; checkpoint: +Amend/Revise) in reviewer tooling.
- Never default ambiguous handler input to Amend — default to asking the operator.
When it happens
Trigger: Calling broker resolve() with ApprovalDecision::Amend { .. } or Revise { .. } on a run whose park is an approval gate rather than a deterministic checkpoint — e.g. an approval UI that offers amend buttons for every parked run.
Common situations: Approval tooling that reuses one decision handler for both checkpoint and gate parks; state machine drift after upgrading semantics of Amend/Revise; automated responder defaulting to Amend on ambiguous content; misrouted run ids between a checkpoint reviewer and gate approver.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- cannot record approval vote for run {run_id}: its parked sna
- amqp.{}: dispatch = {:?} routes to the SOP engine but no SOP
- cannot record checkpoint approval vote for run {run_id}: its
- Gateway responded {status}: {detail}
- terminal persistence failed for run {}; active run and admis
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/896c38d6b79d8c75.
Report an issue: GitHub.