jdx/mise · error

origin changed while preparing publication; reconcile again

Error message

origin changed while preparing publication; reconcile again

What it means

During history-sync publication, the locally saved sync state (Heads) is re-read and compared against the local and remote heads that were captured when publication was planned. If the saved origin head no longer matches what was planned, mise refuses to publish so it never pushes on top of stale assumptions. This prevents racing another sync/pull process that changed the saved history between planning and publishing.

Solutions

  1. Re-run `mise dot sync` so planning and publication happen in one pass with fresh heads.
  2. Check for and finish any other running `mise dot sync`/`mise dot pull` process before retrying.
  3. If the origin really changed intentionally, run `mise dot pull` first to reconcile with the new origin state.

Example fix

// before: plan and publish as separate long-lived steps with stale heads
let plan = plan_publication(&repo, &heads)?;
// ...long work...
publish(&repo, &plan, &stale_expected_local, &stale_upstream)?;
// after: re-read heads immediately before publishing, or retry the whole sync
let heads = Heads::read(repo)?;
if heads.remote.as_deref() != expected_upstream {
    return sync::run(store, tracked); // reconcile again with fresh state
}
Defensive patterns

Strategy: retry

Validate before calling

// re-read and compare heads before publishing
let heads = Heads::read(repo)?;
if heads.remote.as_deref() != expected_upstream || heads.local.as_deref() != expected_local {
    eprintln!("state changed; rerun sync");
}

Type guard

fn heads_unchanged(heads: &Heads, expected_local: Option<&str>, upstream: Option<&str>) -> bool {
    heads.local.as_deref() == expected_local && heads.remote.as_deref() == upstream
}

Try / catch

match run_sync() {
    Err(e) if e.to_string().contains("reconcile again") => retry_sync_with_fresh_heads(),
    Err(e) => return Err(e),
    Ok(v) => Ok(v),
}

Prevention

When it happens

Trigger: Calling the dot sync/publish path when the saved remote head in the state store was updated between plan and publish time — e.g. another `mise dot sync` or `mise dot pull` ran concurrently and changed heads.remote, or the user reconnected/changed the origin between the two steps.

Common situations: Two terminals or an editor plugin and a scheduled sync job both running `mise dot sync`; running publish right after changing origin with `mise dot origin set`; stale long-running publish that overlaps a pull.

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


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/72aebf4185dbda43. Report an issue: GitHub.

Appendix: source

Thrown at src/system/history/sync/publish.rs:24

use super::graph::Heads;
use super::network::{PushOutcome, Remote};
use crate::system::history::shadow::HistoryRepo;

/// Reconciliation and complete live application must precede publication.
/// A merge that still changes this machine's saved tree needs another pull.
pub(crate) fn build(
    repo: &HistoryRepo,
    upstream: Option<&str>,
    expected_local: Option<&str>,
    accepted: &BTreeSet<String>,
    allow_plaintext_history: bool,
) -> Result<Option<String>> {
    let heads = Heads::read(repo)?;
    if heads.local.as_deref() != expected_local {
        bail!("local saved history changed while planning; reconcile again");
    }
    if heads.remote.as_deref() != upstream {
        bail!("origin changed while preparing publication; reconcile again");
    }
    let Some(local) = &heads.local else {
        return Ok(None);
    };
    if heads.remote.as_ref() == Some(local) {
        return Ok(None);
    }
    let tree = repo.output_tree_of(local)?;
    if let Some(remote) = &heads.remote
        && heads.base.as_ref() != Some(remote)
    {
        let (mut merged, mut conflicts) = repo.merge_tree(local, remote)?;
        // Enrollment is a keyed inventory, not arbitrary JSON text. Git's
        // line merge can conflict on independent additions or combine policy
        // changes into invalid metadata. Validate its structured merge first.
        if let Some(base) = &heads.base {
            use crate::system::history::manifest::Manifest;
            if let (Some(base), Some(ours), Some(theirs)) = (

View on GitHub (pinned to 533346cc37)