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
- Re-capture a fresh proposal against the now-existing SOP so it becomes an update carrying the current content hash.
- If the existing SOP is wrong and the proposal should win, delete the target SOP and re-propose (never blind-apply).
- 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
- Check target existence before applying Create proposals.
- Apply proposals promptly after capture to shrink the race window.
- Coordinate teams so only one source creates a given SOP name.
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
- proposal {} is stale; inspect and re-propose
- proposal rejected: {reason}
- proposal {} is {:?}, not pending
- proposal {} quarantined: {reason}
- cannot rename agent memory to `{to}`: an existing memory sto
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/532a9bb177220d4a.
Report an issue: GitHub.