zeroclaw-labs/zeroclaw · error · anyhow::Error
only completed SOP runs can be captured
Error message
only completed SOP runs can be captured
What it means
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.
Source
Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:94
status_reason: None,
applied_at: None,
applied_by: None,
rollback_path: None,
};
engine.save_proposal(&proposal)?;
Ok(proposal)
}
pub fn capture_successful_run(
engine: &SopEngine,
run_id: &str,
requested_by: Option<String>,
) -> Result<ProposalRecord> {
let run = engine
.get_run(run_id)
.ok_or_else(|| anyhow::Error::msg(format!("SOP run not found: {run_id}")))?;
if run.status != SopRunStatus::Completed {
bail!("only completed SOP runs can be captured");
}
if run.step_results.is_empty() {
bail!("completed run has no step results to distill");
}
if run
.step_results
.iter()
.any(|step| matches!(step.status, super::types::SopStepStatus::Failed))
{
bail!("failed step output is not captured into procedural memory");
}
let sop = engine
.get_sop(&run.sop_name)
.ok_or_else(|| anyhow::Error::msg(format!("SOP not loaded: {}", run.sop_name)))?;
let manifest_toml = read_or_default_manifest(sop)?;
let procedure_markdown = append_captured_notes(sop, run_id, &run.step_results)?;
create_proposal(View on GitHub (pinned to 88bb9c8533)
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.
Example fix
// before: capture fired from a step-finished webhook, run not terminal yet
let proposal = capture_successful_run(&engine, run_id, None).await?;
// after: wait for the terminal status first
loop {
let run = engine.get_run(run_id).context("run vanished")?;
if run.status == SopRunStatus::Completed { break; }
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
let proposal = capture_successful_run(&engine, run_id, None).await?; Defensive patterns
Strategy: validation
Validate before calling
let Some(run) = engine.get_run(run_id) else {
anyhow::bail!("SOP run not found: {run_id}");
};
if run.status != SopRunStatus::Completed {
anyhow::bail!("run is {:?}; capture only Completed runs", run.status);
}
capture_successful_run(&engine, run_id, None).await?; Type guard
fn is_capturable(run: &SopRun) -> bool {
run.status == SopRunStatus::Completed && !run.step_results.is_empty()
} Try / catch
match capture_successful_run(&engine, run_id, None).await {
Err(e) if e.to_string().contains("only completed SOP runs") => {
// poll status to Completed, then retry capture once
}
other => other?,
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- completed run has no step results to distill
- failed step output is not captured into procedural memory
- Run {} is not paused at checkpoint (status: {})
- proposal rejected: {reason}
- proposal {} is {:?}, not pending
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/f4221efc27a75696.
Report an issue: GitHub.