{"record":{"id":"4a0900bc125f5957","repo":"zeroclaw-labs/zeroclaw","slug":"proposal-is-stale-inspect-and-re-propose","errorCode":null,"errorMessage":"proposal {} is stale; inspect and re-propose","messagePattern":"proposal (.+?) is stale; inspect and re-propose","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":172,"sourceCode":"    let current_target_hash = if target_dir.exists() {\n        Some(hash_sop_dir(&target_dir)?)\n    } else {\n        None\n    };\n    match (&proposal.kind, &proposal.target_content_hash) {\n        (ProposalKind::Create, None) if current_target_hash.is_some() => {\n            proposal.status = ProposalStatus::Stale;\n            proposal.updated_at = now_iso8601();\n            proposal.status_reason = Some(\"target SOP was created after proposal capture\".into());\n            engine.save_proposal(&proposal)?;\n            bail!(\"proposal {} is stale; target SOP now exists\", proposal.id);\n        }\n        (_, Some(expected)) if current_target_hash.as_ref() != Some(expected) => {\n            proposal.status = ProposalStatus::Stale;\n            proposal.updated_at = now_iso8601();\n            proposal.status_reason = Some(\"target SOP changed since proposal capture\".into());\n            engine.save_proposal(&proposal)?;\n            bail!(\"proposal {} is stale; inspect and re-propose\", proposal.id);\n        }\n        _ => {}\n    }\n\n    if let Some(reason) = scan_candidate(&proposal.manifest_toml, &proposal.procedure_markdown) {\n        proposal.status = ProposalStatus::Quarantined;\n        proposal.updated_at = now_iso8601();\n        proposal.status_reason = Some(reason.clone());\n        engine.save_proposal(&proposal)?;\n        bail!(\"proposal {} quarantined: {reason}\", proposal.id);\n    }\n\n    validate_candidate(\n        &proposal.sop_name,\n        &proposal.manifest_toml,\n        &proposal.procedure_markdown,\n    )?;\n    let rollback = write_rollback(&sops_root, &target_dir, &proposal.id)?;","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"Long review queues where the SOP is hotfixed while proposals wait; multiple operators proposing concurrently; stale proposal records replayed after a refactor.","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."],"exampleFix":"// before: apply a proposal captured before an unrelated SOP edit\napply_proposal(&engine, install_root, id, None).await?; // hash mismatch -> Stale\n\n// after: on stale, re-capture and apply the refreshed proposal\nmatch apply_proposal(&engine, install_root, id, None).await {\n    Ok(out) => out,\n    Err(e) if e.to_string().contains(\"inspect and re-propose\") => {\n        let fresh = capture_successful_run(&engine, &run_id, None).await?;\n        apply_proposal(&engine, install_root, &fresh.id, None).await?\n    }\n    Err(e) => return Err(e),\n}","handlingStrategy":"try-catch","validationCode":"// Best-effort: freeze target SOP edits while proposals are pending, and on any\n// mismatch re-capture instead of forcing the apply.\nlet proposal = engine.load_proposal(id)?.context(\"proposal missing\")?;\nif let Some(expected) = &proposal.target_content_hash {\n    let current = current_target_hash(&engine, &proposal.sop_name); // helper over loaded SOP content\n    if current.as_ref() != Some(expected) {\n        anyhow::bail!(\"target changed since capture; re-propose\");\n    }\n}","typeGuard":null,"tryCatchPattern":"match apply_proposal(&engine, install_root, id, None).await {\n    Ok(out) => out,\n    Err(e) if e.to_string().contains(\"stale; inspect and re-propose\") => {\n        // engine persisted status Stale with status_reason; diff and re-capture\n        let fresh = capture_successful_run(&engine, &run_id, None).await?;\n        apply_proposal(&engine, install_root, &fresh.id, None).await?\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["procedural-memory","proposal","stale","hash-mismatch"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}