zeroclaw-labs/zeroclaw · error · anyhow::Error

proposal {} is stale; target SOP now exists

Error message

proposal {} is stale; target SOP now exists

What it means

A Create-kind proposal (captured when the target SOP did not exist) becomes invalid if the target SOP has been created in the meantime. apply_proposal detects current_target_hash.is_some() for a Create proposal, persists status Stale with status_reason 'target SOP was created after proposal capture', and bails. Applying a create over an existing SOP would clobber content the proposal never saw.

Source

Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:165

            .and_then(|sop| sop.location.clone())
        {
            Some(location) => contained_existing_dir(&sops_root, &location)?,
            None => contained_sop_dir(&sops_root, &proposal.sop_name)?,
        },
        ProposalKind::Create => contained_sop_dir(&sops_root, &proposal.sop_name)?,
    };
    let current_target_hash = if target_dir.exists() {
        Some(hash_sop_dir(&target_dir)?)
    } else {
        None
    };
    match (&proposal.kind, &proposal.target_content_hash) {
        (ProposalKind::Create, None) if current_target_hash.is_some() => {
            proposal.status = ProposalStatus::Stale;
            proposal.updated_at = now_iso8601();
            proposal.status_reason = Some("target SOP was created after proposal capture".into());
            engine.save_proposal(&proposal)?;
            bail!("proposal {} is stale; target SOP now exists", proposal.id);
        }
        (_, Some(expected)) if current_target_hash.as_ref() != Some(expected) => {
            proposal.status = ProposalStatus::Stale;
            proposal.updated_at = now_iso8601();
            proposal.status_reason = Some("target SOP changed since proposal capture".into());
            engine.save_proposal(&proposal)?;
            bail!("proposal {} is stale; inspect and re-propose", proposal.id);
        }
        _ => {}
    }

    if let Some(reason) = scan_candidate(&proposal.manifest_toml, &proposal.procedure_markdown) {
        proposal.status = ProposalStatus::Quarantined;
        proposal.updated_at = now_iso8601();
        proposal.status_reason = Some(reason.clone());
        engine.save_proposal(&proposal)?;
        bail!("proposal {} quarantined: {reason}", proposal.id);
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-capture a fresh proposal against the now-existing SOP so it becomes an update carrying the current content hash.
  2. If the existing SOP is wrong and the proposal should win, delete the target SOP and re-propose (never blind-apply).
  3. Reduce the window between propose and apply, and check engine.get_sop(&proposal.sop_name) before applying a Create.

Example fix

// before: apply a Create proposal after the target was authored manually
apply_proposal(&engine, install_root, id, None).await?; // target appeared -> Stale

// after: pre-check the target and refresh the proposal
if engine.get_sop(&proposal.sop_name).is_some() {
    // target now exists: capture an update proposal from the current SOP instead
    let fresh = capture_successful_run(&engine, &run_id, None).await?;
    apply_proposal(&engine, install_root, &fresh.id, None).await?;
} else {
    apply_proposal(&engine, install_root, id, None).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

let proposal = engine
    .load_proposal(proposal_id)?
    .ok_or_else(|| anyhow::anyhow!("proposal not found"))?;
if matches!(proposal.kind, ProposalKind::Create) && engine.get_sop(&proposal.sop_name).is_some() {
    anyhow::bail!("target SOP '{}' now exists; re-propose as an update", proposal.sop_name);
}
apply_proposal(&engine, install_root, proposal_id, None).await?;

Try / catch

match apply_proposal(&engine, install_root, id, None).await {
    Err(e) if e.to_string().contains("target SOP now exists") => {
        // engine persisted the proposal as Stale; capture a fresh update proposal
    }
    other => other?,
}

Prevention

When it happens

Trigger: Capturing a proposal for a brand-new SOP, then someone authors that SOP manually (or another proposal's apply creates it) before this proposal is applied.

Common situations: Two proposals for the same new SOP name; a reviewer creating the SOP by hand while a captured proposal waits for approval; name collisions between teams.

Related errors


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