zeroclaw-labs/zeroclaw · error · anyhow::Error
proposal {} is stale; inspect and re-propose
Error message
proposal {} is stale; inspect and re-propose What it means
Every update-kind proposal carries target_content_hash, the hash of the target SOP's content at capture time. At apply, if the SOP's current hash differs from the expected one, apply_proposal persists status Stale with status_reason 'target SOP changed since proposal capture' and refuses: the proposal was authored against content that no longer exists, so applying it would silently revert someone else's edits.
Source
Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:172
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);
}
validate_candidate(
&proposal.sop_name,
&proposal.manifest_toml,
&proposal.procedure_markdown,
)?;
let rollback = write_rollback(&sops_root, &target_dir, &proposal.id)?;View on GitHub (pinned to 88bb9c8533)
Solutions
- Re-capture the proposal from the current SOP state and apply the fresh one.
- Stop editing the target SOP between propose and apply (or re-propose immediately after any edit).
- On this error, read the persisted status_reason and diff the SOP against the proposal content before deciding.
Example fix
// before: apply a proposal captured before an unrelated SOP edit
apply_proposal(&engine, install_root, id, None).await?; // hash mismatch -> Stale
// after: on stale, re-capture and apply the refreshed proposal
match apply_proposal(&engine, install_root, id, None).await {
Ok(out) => out,
Err(e) if e.to_string().contains("inspect and re-propose") => {
let fresh = capture_successful_run(&engine, &run_id, None).await?;
apply_proposal(&engine, install_root, &fresh.id, None).await?
}
Err(e) => return Err(e),
} Defensive patterns
Strategy: try-catch
Validate before calling
// Best-effort: freeze target SOP edits while proposals are pending, and on any
// mismatch re-capture instead of forcing the apply.
let proposal = engine.load_proposal(id)?.context("proposal missing")?;
if let Some(expected) = &proposal.target_content_hash {
let current = current_target_hash(&engine, &proposal.sop_name); // helper over loaded SOP content
if current.as_ref() != Some(expected) {
anyhow::bail!("target changed since capture; re-propose");
}
} Try / catch
match apply_proposal(&engine, install_root, id, None).await {
Ok(out) => out,
Err(e) if e.to_string().contains("stale; inspect and re-propose") => {
// engine persisted status Stale with status_reason; diff and re-capture
let fresh = capture_successful_run(&engine, &run_id, None).await?;
apply_proposal(&engine, install_root, &fresh.id, None).await?
}
Err(e) => return Err(e),
} Prevention
- Freeze edits to a target SOP while a proposal against it is pending review.
- Re-propose immediately after any target edit instead of applying stale proposals.
- After this error, always read the persisted status_reason before deciding.
When it happens
Trigger: Editing the target SOP (manifest or procedure) between proposal capture and apply; two proposals targeting the same SOP where the first apply changes the hash for the second; re-applying an old proposal after the SOP evolved.
Common situations: Long review queues where the SOP is hotfixed while proposals wait; multiple operators proposing concurrently; stale proposal records replayed after a refactor.
Related errors
- proposal {} is stale; target SOP now exists
- proposal rejected: {reason}
- proposal {} is {:?}, not pending
- 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/4a0900bc125f5957.
Report an issue: GitHub.