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

cannot migrate backwards from V{from} to V{target}

Error message

cannot migrate backwards from V{from} to V{target}

What it means

run_chain_until only applies forward migration steps (it slices MIGRATION_STEPS[from..target]), so it bails immediately when the requested target is lower than the source version. Downgrades are unsupported because individual steps are not reversible.

Source

Thrown at crates/zeroclaw-config/src/migration.rs:826

        v2.migrate().context("failed to migrate V2 → V3")
    },
];

const _: () = assert!(
    MIGRATION_STEPS.len() as u32 == CURRENT_SCHEMA_VERSION,
    "MIGRATION_STEPS must have exactly one entry per schema version \
     (length = CURRENT_SCHEMA_VERSION, including the slot-0 padding)",
);

/// Run the typed migration chain from `from` up to `CURRENT_SCHEMA_VERSION`.
/// `from` must be `< CURRENT_SCHEMA_VERSION` (caller checks).
fn run_chain(value: toml::Value, from: u32) -> Result<toml::Value> {
    run_chain_until(value, from, CURRENT_SCHEMA_VERSION)
}

fn run_chain_until(value: toml::Value, from: u32, target: u32) -> Result<toml::Value> {
    if target < from {
        anyhow::bail!("cannot migrate backwards from V{from} to V{target}");
    }
    if target > CURRENT_SCHEMA_VERSION {
        anyhow::bail!(
            "target V{target} exceeds CURRENT_SCHEMA_VERSION (V{CURRENT_SCHEMA_VERSION})"
        );
    }

    let mut cur = value;
    for step in &MIGRATION_STEPS[from as usize..target as usize] {
        cur = step(cur)?;
    }
    Ok(cur)
}

pub(crate) fn sync_table(doc: &mut toml_edit::Table, new: &toml::Table) {
    // Drop keys not present in new
    let to_remove: Vec<String> = doc
        .iter()

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Do not request a downgrade; target CURRENT_SCHEMA_VERSION and keep configs forward-only.
  2. If you truly need an old-format config, generate a default config for that version and copy values over by hand.
  3. Restore a pre-upgrade backup of the config instead of migrating back.
  4. Clamp and validate the target argument before calling so user input can never express target < from.

Example fix

// before
let migrated = run_chain_until(value, /* from */ 3, /* target */ 2)?; // cannot migrate backwards

// after: keep configs forward-only and validate the request
if target < from { anyhow::bail!("downgrade not supported"); }
let migrated = run_chain_until(value, from, CURRENT_SCHEMA_VERSION)?;
Defensive patterns

Strategy: validation

Validate before calling

// before calling run_chain_until / generate
if target < from {
    return Err(anyhow::anyhow!("downgrades unsupported: V{from} -> V{target}"));
}
if target > CURRENT_SCHEMA_VERSION {
    return Err(anyhow::anyhow!("target above CURRENT_SCHEMA_VERSION"));
}

Try / catch

match run_chain_until(value, from, target) {
    Err(e) if e.to_string().contains("cannot migrate backwards") => {
        // regenerate a default config for the older version instead
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling run_chain_until(value, from, target) with target < from — for example generate() targeting V2 from a V3 document, or run_chain invoked after detect_version returned a from above the requested target.

Common situations: Tooling that tries to produce a config for an older zeroclaw by asking the migration engine to run backwards; a version parsed from user input or config forwarded into the target without clamping.

Related errors


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