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

  1. Read the reason suffix: 'execution slots full' / 'pending-approval pool full' / 'held' -> Defer: retry once capacity frees, or raise the corresponding limit.
  2. 'not loaded' -> check the SOP name and that the SOP file is loaded/enabled.
  3. 'in cooldown' -> wait out the cooldown window or review the SOP's cooldown setting if replays are legitimate.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/2ed1c60146f9f2e2. Report an issue: GitHub.