zeroclaw-labs/zeroclaw · warning · anyhow::Error
SOP '{sop_name}' not started: {reason}
Error message
SOP '{sop_name}' not started: {reason} What it means
enforce_admission() saw Defer or Drop from evaluate_admission and bails with the embedded reason. Defer means backpressure: execution slots full (per-SOP or global), pending-approval pool full, or Hold policy with a run in flight. Drop means terminal refusal: SOP not loaded, SOP in cooldown, or Drop policy with slots full.
Source
Thrown at crates/zeroclaw-runtime/src/sop/engine.rs:1581
}
}
/// Start a new SOP run. Returns the first action to take.
/// Deterministic SOPs are automatically routed to `start_deterministic_run`.
/// Enforce the SOP's admission policy at a start entrypoint. `Admit` proceeds;
/// any other outcome declines the start with a descriptive error so a trigger is
/// never run past its policy. dispatch pre-consults `evaluate_admission` and only
/// reaches a start path on `Admit`, so re-checking here (under the same held lock)
/// is idempotent; a DIRECT caller (`sop_execute`, or `start_deterministic_run`)
/// would otherwise bypass Hold / Coalesce / the `max_pending_approvals` pool.
fn enforce_admission(&self, sop_name: &str) -> Result<()> {
match self.evaluate_admission(sop_name) {
SopAdmission::Admit => Ok(()),
SopAdmission::Coalesce { existing_run_id } => bail!(
"SOP '{sop_name}' not started: coalesced into in-flight run {existing_run_id}"
),
SopAdmission::Defer { reason } | SopAdmission::Drop { reason } => {
bail!("SOP '{sop_name}' not started: {reason}")
}
}
}
fn rollback_failed_start(
&mut self,
run_id: &str,
claim: &ClaimToken,
err: anyhow::Error,
) -> anyhow::Error {
if err.is::<TerminalPersistenceRetained>() {
return err;
}
self.active_runs.remove(run_id);
self.release_claim_best_effort(claim);
err
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the reason suffix: 'execution slots full' / 'pending-approval pool full' / 'held' -> Defer: retry once capacity frees, or raise the corresponding limit.
- 'not loaded' -> check the SOP name and that the SOP file is loaded/enabled.
- 'in cooldown' -> wait out the cooldown window or review the SOP's cooldown setting if replays are legitimate.
- For sustained load, move triggers to the dispatch API so Defer/Drop/Coalesce are handled as outcomes, not errors.
Example fix
# before: SOP has max_concurrent = 1, a gated run holds the slot zeroclaw sop execute deploy # error: SOP 'deploy' not started: held (a run is already in flight) # after zeroclaw runs list --sop deploy # resolve/finish the in-flight run first, then: zeroclaw sop execute deploy # or raise limits: max_concurrent = 2, max_pending_approvals = 4
Defensive patterns
Strategy: retry
Validate before calling
match engine.evaluate_admission(sop_name) {
SopAdmission::Admit => engine.start_sop(sop_name, input),
SopAdmission::Coalesce { existing_run_id } => follow_run(existing_run_id),
SopAdmission::Defer { reason } => retry_later(reason),
SopAdmission::Drop { reason } => surface_terminal(reason), // not retryable as-is
} Try / catch
match engine.start_sop(sop_name, input) {
Err(e) if e.to_string().contains("not started:") => {
let reason = e.to_string().rsplit(": ").next().unwrap_or("");
if reason.contains("cooldown") || reason.contains("not loaded") {
return Err(e); // terminal: fix config/name or wait out cooldown
}
schedule_retry(Duration::from_secs(5)) // Defer backpressure: retry with backoff
}
rest => rest?,
} Prevention
- Route bursty triggers through dispatch with an explicit admission policy; handle Defer/Drop as outcomes.
- Alert on pending-approval pool saturation — it usually means approvers are not keeping up.
- Verify SOP names against loaded SOPs before starting; keep cooldown windows shorter than your worst-case replay.
When it happens
Trigger: Direct SOP start when evaluate_admission returns Defer (max_concurrent/max_concurrent_total reached, max_pending_approvals pool full, Hold with an active run) or Drop (SOP name not loaded, cooldown active after a prior run, admission_policy=Drop with slots full). The specific reason string is included in the message.
Common situations: Trigger storms saturating execution slots; approval pool full because gated runs await quorum; SOP disabled/renamed so it is not loaded; debouncing cooldown rejecting replays; Hold policy blocking concurrent manual runs.
Related errors
- Cannot start SOP '{}': cooldown or concurrency limit reached
- SOP '{sop_name}' not started: coalesced into in-flight run {
- run {} ({}) cannot resume yet: execution slots are full; it
- rename_agent not supported by this memory backend
- amqp.{}: dispatch = {:?} routes to the SOP engine but no SOP
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/2ed1c60146f9f2e2.
Report an issue: GitHub.