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

  1. Load the proposal first (engine.load_proposal(id)) and only apply when status is Pending.
  2. If it is already Applied, treat the retry as success (idempotency in the caller).
  3. 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

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


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