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-band

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Send ApprovalDecision::Approve or Deny for runs parked at approval gates.
  2. Gate the Amend/Revise UI on the park kind — only offer them for deterministic checkpoints.
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/896c38d6b79d8c75. Report an issue: GitHub.