zeroclaw-labs/zeroclaw · error · anyhow::Error

SOP '{}' is not in deterministic mode (mode: {})

Error message

SOP '{}' is not in deterministic mode (mode: {})

What it means

The deterministic direct-run entry only accepts SOPs whose execution_mode is SopExecutionMode::Deterministic. The mode is checked before an execution slot is reserved, so a wrong-mode direct call cannot claim (and then have to roll back) a slot. Any other mode (e.g. supervised/agent-driven) must go through the normal start-and-approve flow.

Source

Thrown at crates/zeroclaw-runtime/src/sop/engine.rs:3684

        // / the pending-approval pool that `start_run` enforces. (When reached via
        // `start_run` the re-check is idempotent under the same held lock.)
        self.enforce_admission(sop_name)?;

        let sop = self.get_sop(sop_name).ok_or_else(|| {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                    .with_attrs(::serde_json::json!({"sop_name": sop_name})),
                "SOP engine: sop not found"
            );
            anyhow::Error::msg(format!("SOP not found: {sop_name}"))
        })?;

        // Reject a non-deterministic SOP BEFORE reserving a slot, so a wrong-mode direct
        // call cannot claim (and then have to roll back) an execution slot.
        if sop.execution_mode != SopExecutionMode::Deterministic {
            bail!(
                "SOP '{}' is not in deterministic mode (mode: {})",
                sop_name,
                sop.execution_mode
            );
        }

        // Reserve + activate through the shared two-phase start path (identical run_id
        // prefix, logging, and dispatch to the pre-refactor inline body).
        let reservation = self.reserve_run_slot(sop_name)?;
        self.activate_reserved_run(reservation, event)
    }

    pub fn drive_headless_deterministic(
        &mut self,
        run_id: &str,
        first_action: SopRunAction,
    ) -> Result<SopRunAction> {
        let mut action = first_action;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Use the normal SOP start/approval flow for non-deterministic SOPs instead of the deterministic direct entry.
  2. If unattended direct runs are intended, set execution_mode = "deterministic" in the SOP's SOP.toml [sop] section.
  3. Check engine.get_sop(name).execution_mode before calling, and route to the right entry based on the mode.

Example fix

# before - SOP.toml
[sop]
name = "nightly-backup"
execution_mode = "supervised"

# after - SOP is meant to run unattended via the deterministic entry
[sop]
name = "nightly-backup"
execution_mode = "deterministic"
Defensive patterns

Strategy: validation

Validate before calling

let sop = engine
    .get_sop(sop_name)
    .ok_or_else(|| anyhow::anyhow!("SOP not found: {sop_name}"))?;
if sop.execution_mode != SopExecutionMode::Deterministic {
    anyhow::bail!("use the start/approve flow for {}-mode SOPs", sop.execution_mode);
}
engine.run_deterministic(/* ... */).await?;

Type guard

fn is_deterministic(sop: &Sop) -> bool {
    sop.execution_mode == SopExecutionMode::Deterministic
}

Try / catch

match engine.run_deterministic(/* ... */).await {
    Err(e) if e.to_string().contains("is not in deterministic mode") => {
        // route this SOP through the supervised start/approve flow instead
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling the deterministic run entry with an SOP whose manifest declares a non-deterministic execution_mode (for example 'supervised'), or after someone edited SOP.toml to change the mode without updating callers.

Common situations: Automation scripts wired to the direct-run API while the SOP was authored for interactive approval; renaming or re-authoring an SOP with a different mode; copy-pasting a trigger/config from a deterministic SOP onto a supervised one.

Related errors


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