zeroclaw-labs/zeroclaw · error · anyhow::Error
proposal {} is {:?}, not pending
Error message
proposal {} is {:?}, not pending What it means
apply_proposal loads the proposal record and requires status Pending before doing anything. Proposals that were already applied, marked stale, quarantined, or rejected are refused; the message names the proposal id and its actual status. Applying is a one-shot state transition, not an idempotent retry.
Source
Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:133
sop_name: sop.name.clone(),
description: sop.description.clone(),
manifest_toml: Some(manifest_toml),
procedure_markdown,
source_run_id: Some(run_id.to_string()),
requested_by,
},
)
}
pub fn apply_proposal(
engine: &mut SopEngine,
install_root: &Path,
proposal_id: &str,
applied_by: Option<String>,
) -> Result<ApplyOutcome> {
let mut proposal = load_required(engine, proposal_id)?;
if proposal.status != ProposalStatus::Pending {
bail!(
"proposal {} is {:?}, not pending",
proposal.id,
proposal.status
);
}
let sops_root = super::resolve_sops_dir(install_root, engine.config().sops_dir.as_deref());
// An Update must land on the currently-loaded SOP's actual directory, which
// the loader sets from on-disk layout and need not match a slug of the name.
// Create has no loaded SOP, so derive the new directory from the name.
let target_dir = match proposal.kind {
ProposalKind::Update => match engine
.get_sop(&proposal.sop_name)
.and_then(|sop| sop.location.clone())
{
Some(location) => contained_existing_dir(&sops_root, &location)?,
None => contained_sop_dir(&sops_root, &proposal.sop_name)?,
},View on GitHub (pinned to 88bb9c8533)
Solutions
- Load the proposal first (engine.load_proposal(id)) and only apply when status is Pending.
- If it is already Applied, treat the retry as success (idempotency in the caller).
- If Stale or Quarantined, read status_reason and create a fresh proposal instead of re-applying.
Example fix
// before: apply retried on timeout, but the first attempt already succeeded
apply_proposal(&engine, install_root, proposal_id, None).await?;
// after: make apply idempotent on the caller side
let p = engine.load_proposal(proposal_id)?.context("proposal missing")?;
match p.status {
ProposalStatus::Pending => { apply_proposal(&engine, install_root, proposal_id, None).await?; }
ProposalStatus::Applied => { tracing::info!("already applied"); }
other => anyhow::bail!("proposal {other:?} not appliable; re-propose"),
} Defensive patterns
Strategy: validation
Validate before calling
let proposal = engine
.load_proposal(proposal_id)?
.ok_or_else(|| anyhow::anyhow!("proposal not found: {proposal_id}"))?;
anyhow::ensure!(
proposal.status == ProposalStatus::Pending,
"proposal is {:?}; only Pending proposals apply",
proposal.status
); Type guard
fn is_pending(p: &ProposalRecord) -> bool {
p.status == ProposalStatus::Pending
} Try / catch
match apply_proposal(&engine, install_root, id, None).await {
Err(e) if e.to_string().contains(", not pending") => {
// reload the record: Applied -> treat as done; Stale/Quarantined -> re-propose
}
other => other?,
} Prevention
- Make apply callers idempotent: Applied on retry is success.
- Check proposal status before applying and after any apply error.
- Guard against duplicate apply deliveries from queues/UI retries.
When it happens
Trigger: Calling apply twice (double-click, queue redelivery); applying a proposal that a previous apply already transitioned to Applied; applying after a stale/quarantine marker was set by an earlier failed attempt.
Common situations: Retried RPC deliveries from channels; operators re-running an apply script; dashboards that resubmit on timeout even though the first apply succeeded.
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
- proposal rejected: {reason}
- proposal {} is stale; target SOP now exists
- proposal {} is stale; inspect and re-propose
- proposal {} quarantined: {reason}
- only completed SOP runs can be captured
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/cbf9e4d101d305c4.
Report an issue: GitHub.