{"record":{"id":"f4221efc27a75696","repo":"zeroclaw-labs/zeroclaw","slug":"only-completed-sop-runs-can-be-captured","errorCode":null,"errorMessage":"only completed SOP runs can be captured","messagePattern":"only completed SOP runs can be captured","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/procedural_memory.rs","lineNumber":94,"sourceCode":"        status_reason: None,\n        applied_at: None,\n        applied_by: None,\n        rollback_path: None,\n    };\n    engine.save_proposal(&proposal)?;\n    Ok(proposal)\n}\n\npub fn capture_successful_run(\n    engine: &SopEngine,\n    run_id: &str,\n    requested_by: Option<String>,\n) -> Result<ProposalRecord> {\n    let run = engine\n        .get_run(run_id)\n        .ok_or_else(|| anyhow::Error::msg(format!(\"SOP run not found: {run_id}\")))?;\n    if run.status != SopRunStatus::Completed {\n        bail!(\"only completed SOP runs can be captured\");\n    }\n    if run.step_results.is_empty() {\n        bail!(\"completed run has no step results to distill\");\n    }\n    if run\n        .step_results\n        .iter()\n        .any(|step| matches!(step.status, super::types::SopStepStatus::Failed))\n    {\n        bail!(\"failed step output is not captured into procedural memory\");\n    }\n\n    let sop = engine\n        .get_sop(&run.sop_name)\n        .ok_or_else(|| anyhow::Error::msg(format!(\"SOP not loaded: {}\", run.sop_name)))?;\n    let manifest_toml = read_or_default_manifest(sop)?;\n    let procedure_markdown = append_captured_notes(sop, run_id, &run.step_results)?;\n    create_proposal(","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/procedural_memory.rs#L76-L112","documentation":"capture_successful_run distills a finished SOP run into a proposal, and only a run whose status is exactly SopRunStatus::Completed qualifies. Runs that are still executing, paused at a checkpoint or gate, failed, or otherwise non-terminal are refused because their step outputs are not a complete, trusted procedure.","triggerScenarios":"Calling capture while the run is mid-flight (Running/PausedCheckpoint), after a failure, or before the final step's result has been recorded. The engine looked the run up successfully (a missing run is a different error) but found a non-Completed status.","commonSituations":"Automation that captures on a completion signal that fires early; capturing from a webhook that races the run's final transition; retrying capture after a failed run hoping to salvage partial output.","solutions":["Wait for the run to reach Completed (poll engine.get_run(run_id).status) before capturing.","If the run failed, fix and re-run it; failed runs are never capturable (see the failed-step guard).","Wire capture to the run's actual completion event rather than an upstream trigger."],"exampleFix":"// before: capture fired from a step-finished webhook, run not terminal yet\nlet proposal = capture_successful_run(&engine, run_id, None).await?;\n\n// after: wait for the terminal status first\nloop {\n    let run = engine.get_run(run_id).context(\"run vanished\")?;\n    if run.status == SopRunStatus::Completed { break; }\n    tokio::time::sleep(std::time::Duration::from_millis(500)).await;\n}\nlet proposal = capture_successful_run(&engine, run_id, None).await?;","handlingStrategy":"validation","validationCode":"let Some(run) = engine.get_run(run_id) else {\n    anyhow::bail!(\"SOP run not found: {run_id}\");\n};\nif run.status != SopRunStatus::Completed {\n    anyhow::bail!(\"run is {:?}; capture only Completed runs\", run.status);\n}\ncapture_successful_run(&engine, run_id, None).await?;","typeGuard":"fn is_capturable(run: &SopRun) -> bool {\n    run.status == SopRunStatus::Completed && !run.step_results.is_empty()\n}","tryCatchPattern":"match capture_successful_run(&engine, run_id, None).await {\n    Err(e) if e.to_string().contains(\"only completed SOP runs\") => {\n        // poll status to Completed, then retry capture once\n    }\n    other => other?,\n}","preventionTips":["Trigger capture from the run-completion event, not from upstream webhooks.","Poll engine.get_run until Completed before capturing.","Never attempt to capture failed or paused runs."],"tags":["procedural-memory","capture","run-status"],"backgroundTag":"invalid-state-transition","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}