{"record":{"id":"654510ff8117ac5d","repo":"jdx/mise","slug":"local-saved-history-changed-while-planning-reconc","errorCode":null,"errorMessage":"local saved history changed while planning; reconcile again","messagePattern":"local saved history changed while planning; reconcile again","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/sync/publish.rs","lineNumber":20,"sourceCode":"\nuse eyre::{Result, bail};\nuse std::collections::BTreeSet;\n\nuse super::graph::Heads;\nuse super::network::{PushOutcome, Remote};\nuse crate::system::history::shadow::HistoryRepo;\n\n/// Reconciliation and complete live application must precede publication.\n/// A merge that still changes this machine's saved tree needs another pull.\npub(crate) fn build(\n    repo: &HistoryRepo,\n    upstream: Option<&str>,\n    expected_local: Option<&str>,\n    accepted: &BTreeSet<String>,\n) -> Result<Option<String>> {\n    let heads = Heads::read(repo)?;\n    if heads.local.as_deref() != expected_local {\n        bail!(\"local saved history changed while planning; reconcile again\");\n    }\n    if heads.remote.as_deref() != upstream {\n        bail!(\"origin changed while preparing publication; reconcile again\");\n    }\n    let Some(local) = &heads.local else {\n        return Ok(None);\n    };\n    if heads.remote.as_ref() == Some(local) {\n        return Ok(None);\n    }\n    let tree = repo.output_tree_of(local)?;\n    if let Some(remote) = &heads.remote\n        && heads.base.as_ref() != Some(remote)\n    {\n        let (mut merged, mut conflicts) = repo.merge_tree(local, remote)?;\n        // Enrollment is a keyed inventory, not arbitrary JSON text. Git's\n        // line merge can conflict on independent additions or combine policy\n        // changes into invalid metadata. Validate its structured merge first.","sourceCodeStart":2,"sourceCodeEnd":38,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/sync/publish.rs#L2-L38","documentation":"Before publishing saved history, mise re-reads the shadow repository's heads and compares the local head against the value captured when the plan was built (`expected_local`). If the local branch moved between planning and publication — another sync, pull, or local write happened concurrently — mise aborts and asks the user to reconcile again, preventing publication based on stale assumptions.","triggerScenarios":"In publish `build`, `Heads::read(repo).local != expected_local`. Triggered by any concurrent modification of the saved-history branch between the plan/prepare step and the publish step: another mise process pulling or committing, or a changed accepted set applied locally in the meantime.","commonSituations":"Two terminals running mise bootstrap dotfiles pull/publish at once; a background sync job committed saved history while the user was publishing; the user ran a reconcile between the prepare and publish commands.","solutions":["Re-run the reconcile/plan step (`mise bootstrap dotfiles pull` or equivalent) so `expected_local` is refreshed, then publish again.","Ensure no other mise sync process is running concurrently (single writer).","If it recurs, serialize sync operations (script them sequentially) rather than invoking pull and publish in parallel."],"exampleFix":"// before: prepare and publish interleaved with another process\nmise bootstrap dotfiles publish &\nmise bootstrap dotfiles pull            # moves local head -> stale expected_local\n// after\nmise bootstrap dotfiles pull && mise bootstrap dotfiles publish   # sequential, no concurrency","handlingStrategy":"retry","validationCode":"const heads = await readHeads();\nif (heads.local !== expectedLocal) {\n  console.warn(\"local head moved; rerun reconcile before publishing\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await publish();\n} catch (e) {\n  if (String(e.message).includes(\"changed while planning\")) {\n    await reconcile();\n    await publish();   // retry once with a fresh plan\n  } else throw e;\n}","preventionTips":["Run only one mise sync operation at a time; avoid concurrent pull/publish.","Minimize the delay between plan and publish.","Disable background sync jobs while publishing manually."],"tags":["sync","race-condition","git","stale-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}